From 806f6bed8103a2016f7a27495c89d42c27c7cc64 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Mon, 18 Mar 2024 00:19:13 +0100 Subject: [PATCH 01/17] Update autoconsent to v10.3.0 (#2433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1206853951101015/1206853951101015 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v10.3.0 ## Description Updates Autoconsent to version [v10.3.0](https://github.com/duckduckgo/autoconsent/releases/tag/v10.3.0). ### Autoconsent v10.3.0 release notes #### 🚀 Enhancement - DDG release automation [#389](https://github.com/duckduckgo/autoconsent/pull/389) ([@muodov](https://github.com/muodov)) - Bump the dev-dependencies group with 4 updates [#390](https://github.com/duckduckgo/autoconsent/pull/390) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### 🐛 Bug Fix - Fix infinite reload for OneTrust sites [#393](https://github.com/duckduckgo/autoconsent/pull/393) ([@muodov](https://github.com/muodov)) - Script to crawl page text content in multiple languages. [#386](https://github.com/duckduckgo/autoconsent/pull/386) ([@sammacbeth](https://github.com/sammacbeth)) - Update Asana sync action [#388](https://github.com/duckduckgo/autoconsent/pull/388) ([@sammacbeth](https://github.com/sammacbeth)) #### Authors: 3 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Maxim Tsoy ([@muodov](https://github.com/muodov)) - Sam Macbeth ([@sammacbeth](https://github.com/sammacbeth)) ## Steps to test This release has been tested during Autoconsent development. You can check the release notes for more information. Co-authored-by: muodov --- DuckDuckGo/Autoconsent/autoconsent-bundle.js | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index 6994afdcd0..db02dcc60b 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||"moove_gdpr_strict_cookies"===e.name||(e.checked=!1)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!1,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-consent-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-consent-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="privacy-policy"]'},{click:'div[role="dialog"] button:nth-child(2)'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{click:"#cookiescript_reject"}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www|)?\\.csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{click:'div.b-cookies-informer__switchers > div:nth-child(2) > div[at-attr="checkbox"] > span.b-input-radio__container > input[type="checkbox"]'},{click:"div.b-cookies-informer__nav > button"}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!0,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{hide:"#sd-cmp"},{eval:"EVAL_SIRDATA_UNBLOCK_SCROLL"}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:["#cookie_initial_modal",".modal-backdrop"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:"#cookie_initial_modal"}],detectPopup:[{visible:"#cookie_initial_modal"}],optIn:[{click:"button#jss_consent_all_initial_modal"}],optOut:[{click:"button#jss_open_settings_modal"},{click:"button#jss_consent_checked"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"tumblr-com",cosmetic:!0,prehideSelectors:["#cmp-app-container"],detectCmp:[{exists:"#cmp-app-container"}],detectPopup:[{visible:"#cmp-app-container"}],optIn:[{click:"#tumblr #cmp-app-container div.components-modal__frame > iframe > html body > div > div > div.cmp__dialog-footer > div > button.components-button.white-space-normal.is-primary"}],optOut:[{hide:"#cmp-app-container"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.reduce(((e,t)=>t.prehideSelectors?[...e,...t.prehideSelectors]:e),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||"moove_gdpr_strict_cookies"===e.name||(e.checked=!1)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!1,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="privacy-policy"]'},{click:'div[role="dialog"] button:nth-child(2)'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{click:"#cookiescript_reject"}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www|)?\\.csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{click:'div.b-cookies-informer__switchers > div:nth-child(2) > div[at-attr="checkbox"] > span.b-input-radio__container > input[type="checkbox"]'},{click:"div.b-cookies-informer__nav > button"}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:["#cookie_initial_modal",".modal-backdrop"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:"#cookie_initial_modal"}],detectPopup:[{visible:"#cookie_initial_modal"}],optIn:[{click:"button#jss_consent_all_initial_modal"}],optOut:[{click:"button#jss_open_settings_modal"},{click:"button#jss_consent_checked"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"tumblr-com",cosmetic:!0,prehideSelectors:["#cmp-app-container"],detectCmp:[{exists:"#cmp-app-container"}],detectPopup:[{visible:"#cmp-app-container"}],optIn:[{click:"#tumblr #cmp-app-container div.components-modal__frame > iframe > html body > div > div > div.cmp__dialog-footer > div > button.components-button.white-space-normal.is-primary"}],optOut:[{hide:"#cmp-app-container"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.reduce(((e,t)=>t.prehideSelectors?[...e,...t.prehideSelectors]:e),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); diff --git a/package-lock.json b/package-lock.json index 597b71e54c..a506b40d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.2.0" + "@duckduckgo/autoconsent": "^10.3.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.2.0.tgz", - "integrity": "sha512-Q4sSGrvA5nWl5auJzttPQu1t25ff9N8Xj/UYglNKNqcnMAx/KxAIP5KbAFgf7JBru+q9Dq7muaEEB4FPU31fEw==" + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.3.0.tgz", + "integrity": "sha512-dUf37qkaYDuXEytU9mNNLGw28S1t1M1dFnvMHZDV9BpINVJeAl1ye7CmlABuGlDs6URrp2ZLZ5IxcKQhQglYcw==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index 2fb1f7eb4e..ee09d66203 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.2.0" + "@duckduckgo/autoconsent": "^10.3.0" } } From 9f666aab5bcfd4f5b1ff3495013edd04baca072a Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 18 Mar 2024 08:50:29 +0000 Subject: [PATCH 02/17] Bump version to 1.80.0 (146) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index b11c07fc54..7d20d1f3e4 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 145 +CURRENT_PROJECT_VERSION = 146 From 61ea0c6f8d4a6f2b0cf6befea8bbbac05f13a194 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 18 Mar 2024 10:39:27 +0100 Subject: [PATCH 03/17] When publishing a DMG, only check out the branch if it exists, otherwise stay on main --- .github/workflows/publish_dmg_release.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index fa4d06d4ce..8daecea7be 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -110,12 +110,24 @@ jobs: fi echo "release-version=${TAG//-/.}" >> $GITHUB_OUTPUT + # Always check out main first, because the release branch might have been deleted (for public releases) - name: Check out the code uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log submodules: recursive - ref: ${{ inputs.branch || github.ref_name }} + ref: main + + - name: Check out the branch if it exists + env: + branch: ${{ inputs.branch || github.ref_name }} + run: | + if [[ -z "${branch}" ]] || git ls-remote --exit-code --heads origin "${branch}"; then + echo "::notice::Checking out ${branch} branch." + git checkout "${branch}" + else + echo "::notice::Branch ${branch} doesn't exist on the remote repository, staying on main." + fi - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer From 655a70c9d1067a53c91147664946e4bc6d31472c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Mon, 18 Mar 2024 12:59:22 +0000 Subject: [PATCH 04/17] Use History in Suggestions on iOS (#2339) Task/Issue URL: https://app.asana.com/0/0/1206524433066958/f Tech Design URL: CC: **Description**: BSK updated with API used by iOS, with minor changes reflected here. **Steps to test this PR**: 1. Launch the app 2. Smoke test history feature in particular 3. General smoke testing --- DuckDuckGo.xcodeproj/project.pbxproj | 26 ++++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +-- DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 3 ++- .../View/AddressBarTextField.swift | 2 +- .../Model/SuggestionContainer.swift | 6 ++--- .../ViewModel/SuggestionViewModel.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Model/SuggestionContainerTests.swift | 2 +- .../Model/SuggestionLoadingMock.swift | 2 +- .../SuggestionContainerViewModelTests.swift | 2 +- .../ViewModel/SuggestionViewModelTests.swift | 2 +- 13 files changed, 41 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7cb377c93b..605a9fee5a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2358,6 +2358,9 @@ 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; }; 85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D33F1125C82EB3002B91A6 /* ConfigurationManager.swift */; }; 85D438B6256E7C9E00F3BAF8 /* ContextMenuUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */; }; + 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 85D44B852BA08D29001B4AB5 /* Suggestions */; }; + 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 85D44B872BA08D30001B4AB5 /* Suggestions */; }; + 85D44B8A2BA08D3B001B4AB5 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 85D44B892BA08D3B001B4AB5 /* Suggestions */; }; 85D885B026A590A90077C374 /* NSNotificationName+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885AF26A590A90077C374 /* NSNotificationName+PasswordManager.swift */; }; 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885B226A5A9DE0077C374 /* NSAlert+PasswordManager.swift */; }; 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBCD2B8F534000DBEC7A /* History */; }; @@ -4536,6 +4539,7 @@ 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */, 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */, + 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, 4BF97AD12B43C43F00EB4240 /* NetworkProtectionIPC in Frameworks */, 37F44A5F298C17830025E7FE /* Navigation in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, @@ -4682,6 +4686,7 @@ 85E2BBD22B8F536F00DBEC7A /* History in Frameworks */, 4B957BE62AC7AE700062CA31 /* PrivacyDashboard in Frameworks */, 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */, + 85D44B8A2BA08D3B001B4AB5 /* Suggestions in Frameworks */, 4B957BE72AC7AE700062CA31 /* SyncDataProviders in Frameworks */, 37269F032B332FD8005E8E46 /* Common in Frameworks */, 4B957BE82AC7AE700062CA31 /* SyncUI in Frameworks */, @@ -4767,6 +4772,7 @@ 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */, 371D00E129D8509400EC8598 /* OpenSSL in Frameworks */, 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */, + 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */, 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, @@ -8466,6 +8472,7 @@ 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, 85E2BBCF2B8F534A00DBEC7A /* History */, F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */, + 85D44B872BA08D30001B4AB5 /* Suggestions */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8753,6 +8760,7 @@ 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, 85E2BBD12B8F536F00DBEC7A /* History */, F1D43AF62B98E48F00BAB743 /* BareBonesBrowserKit */, + 85D44B892BA08D3B001B4AB5 /* Suggestions */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8922,6 +8930,7 @@ 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */, 1EA7B8D42B7E078C000330A4 /* Subscription */, F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */, + 85D44B852BA08D29001B4AB5 /* Suggestions */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -13755,7 +13764,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 125.0.2; + version = 126.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -14281,6 +14290,21 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 85D44B852BA08D29001B4AB5 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; + 85D44B872BA08D30001B4AB5 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; + 85D44B892BA08D3B001B4AB5 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; 85E2BBCD2B8F534000DBEC7A /* History */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 329893ae73..8827764be0 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "810bf41347ff437b5d0154405a238553537240a4", - "version" : "125.0.2" + "revision" : "7656e94efcf4eedf1c16152c63f57fb52b6ad079", + "version" : "126.0.0" } }, { diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 2fe7281adb..994557e2ba 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -19,10 +19,11 @@ import Foundation import BrowserServicesKit import Common +import Suggestions struct BookmarkList { - struct IdentifiableBookmark: Equatable, BrowserServicesKit.Bookmark { + struct IdentifiableBookmark: Equatable, Suggestions.Bookmark { let id: String let url: String let urlObject: URL? diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index c834cdd58e..f75a983cca 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -20,7 +20,7 @@ import AppKit import Carbon.HIToolbox import Combine import Common -import BrowserServicesKit +import Suggestions final class AddressBarTextField: NSTextField { diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index d1be46c375..9ca0f3aaa9 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -17,7 +17,7 @@ // import Foundation -import BrowserServicesKit +import Suggestions import Common import History @@ -86,11 +86,11 @@ final class SuggestionContainer { extension SuggestionContainer: SuggestionLoadingDataSource { - func history(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.HistorySuggestion] { + func history(for suggestionLoading: SuggestionLoading) -> [HistorySuggestion] { return historyCoordinating.history ?? [] } - func bookmarks(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.Bookmark] { + func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { bookmarkManager.list?.bookmarks() ?? [] } diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index e3aac3ea0c..d7b8c5910c 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -17,7 +17,7 @@ // import Cocoa -import BrowserServicesKit +import Suggestions struct SuggestionViewModel: Equatable { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8e19bc15eb..19a07937ac 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 9d711dfba5..380e8a7897 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index cfe19e07ac..2211902822 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index 3db48611ba..db15131a1c 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -17,7 +17,7 @@ // import XCTest -import BrowserServicesKit +import Suggestions @testable import DuckDuckGo_Privacy_Browser final class SuggestionContainerTests: XCTestCase { diff --git a/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift b/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift index 717d0c05b2..30248315c7 100644 --- a/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift +++ b/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift @@ -17,7 +17,7 @@ // import XCTest -import BrowserServicesKit +import Suggestions @testable import DuckDuckGo_Privacy_Browser final class SuggestionLoadingMock: SuggestionLoading { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index 802bc2cf1f..cee0f7bec3 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -18,7 +18,7 @@ import XCTest import Combine -import BrowserServicesKit +import Suggestions @testable import DuckDuckGo_Privacy_Browser final class SuggestionContainerViewModelTests: XCTestCase { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift index af77dca2d5..ae95463fd3 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import BrowserServicesKit +import Suggestions import XCTest @testable import DuckDuckGo_Privacy_Browser From 9b754f7238aa1759cf96f3b8842d30244128566b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 18 Mar 2024 16:13:35 +0100 Subject: [PATCH 05/17] Prevents the tunnel from starting without an auth token (#2438) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206859302875830/f ## Description Does not let the menu app try to start the tunnel without an auth token. Also turns off on-demand if the menu app has no auth token on startup. --- .../NetworkProtectionTunnelController.swift | 8 ++- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 68 ++++++++++--------- DuckDuckGoVPN/NetworkProtectionBouncer.swift | 4 +- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 4132457ca2..619da111ff 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -445,12 +445,15 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Starting & Stopping the VPN enum StartError: LocalizedError { + case noAuthToken case connectionStatusInvalid case connectionAlreadyStarted case simulateControllerFailureError var errorDescription: String? { switch self { + case .noAuthToken: + return "You need a subscription to start the VPN" case .connectionAlreadyStarted: #if DEBUG return "[Debug] Connection already started" @@ -535,7 +538,10 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr var options = [String: NSObject]() options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString - options[NetworkProtectionOptionKey.authToken] = try tokenStore.fetchToken() as NSString? + guard let authToken = try tokenStore.fetchToken() as NSString? else { + throw StartError.noAuthToken + } + options[NetworkProtectionOptionKey.authToken] = authToken options[NetworkProtectionOptionKey.selectedEnvironment] = settings.selectedEnvironment.rawValue as NSString options[NetworkProtectionOptionKey.selectedServer] = settings.selectedServer.stringValue as? NSString diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index f5643c3243..4ca0fe2aa5 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -260,57 +260,61 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { setupMenuVisibility() - bouncer.requireAuthTokenOrKillApp() + Task { @MainActor in + // The reason we want to await for this is that nothing else should be executed + // if the app should quit. + await bouncer.requireAuthTokenOrKillApp(controller: tunnelController) - // Initialize lazy properties - _ = tunnelControllerIPCService - _ = vpnProxyLauncher + // Initialize lazy properties + _ = tunnelControllerIPCService + _ = vpnProxyLauncher - let dryRun: Bool + let dryRun: Bool #if DEBUG - dryRun = true + dryRun = true #else - dryRun = false + dryRun = false #endif - let pixelSource: String + let pixelSource: String #if NETP_SYSTEM_EXTENSION - pixelSource = "vpnAgent" + pixelSource = "vpnAgent" #else - pixelSource = "vpnAgentAppStore" + pixelSource = "vpnAgentAppStore" #endif - PixelKit.setUp(dryRun: dryRun, - appVersion: AppVersion.shared.versionNumber, - source: pixelSource, - defaultHeaders: [:], - log: .networkProtectionPixel, - defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: pixelSource, + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in - let url = URL.pixelUrl(forPixelNamed: pixelName) - let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself - let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) - let request = APIRequest(configuration: configuration) + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) - request.fetch { _, error in - onComplete(error == nil, error) + request.fetch { _, error in + onComplete(error == nil, error) + } } - } - vpnAppEventsHandler.appDidFinishLaunching() + vpnAppEventsHandler.appDidFinishLaunching() - let launchInformation = LoginItemLaunchInformation(agentBundleID: Bundle.main.bundleIdentifier!, defaults: .netP) - let launchedOnStartup = launchInformation.wasLaunchedByStartup - launchInformation.update() + let launchInformation = LoginItemLaunchInformation(agentBundleID: Bundle.main.bundleIdentifier!, defaults: .netP) + let launchedOnStartup = launchInformation.wasLaunchedByStartup + launchInformation.update() - if launchedOnStartup { - Task { - let isConnected = await tunnelController.isConnected + if launchedOnStartup { + Task { + let isConnected = await tunnelController.isConnected - if !isConnected && tunnelSettings.connectOnLogin { - await tunnelController.start() + if !isConnected && tunnelSettings.connectOnLogin { + await tunnelController.start() + } } } } diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 6b64523c3e..52963e7b61 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -29,7 +29,7 @@ final class NetworkProtectionBouncer { /// Simply verifies that the VPN feature is enabled and if not, takes care of killing the /// current app. /// - func requireAuthTokenOrKillApp() { + func requireAuthTokenOrKillApp(controller: TunnelController) async { let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false, @@ -38,6 +38,8 @@ final class NetworkProtectionBouncer { guard keychainStore.isFeatureActivated else { os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized.") + await controller.stop() + // EXIT_SUCCESS ensures the login item won't relaunch // Ref: https://developer.apple.com/documentation/servicemanagement/smappservice/register() // See where it mentions: From 2e80fbfbc8500e64f2f1c9e38714a0bf098ea53e Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 18 Mar 2024 17:51:06 -0300 Subject: [PATCH 06/17] DBP: Make webview non-persistent and delete any old cache data (#2445) --- .../Sources/DataBrokerProtection/CCF/WebViewHandler.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift index ab98fa98b9..f25a39ff74 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift @@ -48,6 +48,7 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { let configuration = WKWebViewConfiguration() configuration.applyDataBrokerConfiguration(privacyConfig: privacyConfig, prefs: prefs, delegate: delegate) configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() self.webViewConfiguration = configuration self.isFakeBroker = isFakeBroker @@ -85,6 +86,9 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { webView?.stopLoading() userContentController?.cleanUpBeforeClosing() + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0)) { + os_log("WKWebView data store deleted correctly", log: .action) + } userContentController = nil webView?.navigationDelegate = nil From ba291c1ba3f253f54cd8223c3e48acf8cd3415e3 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:03:08 -0400 Subject: [PATCH 07/17] Remove hardcoded NetP staging endpoint (#2446) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 605a9fee5a..a5457be403 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13764,7 +13764,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.0.0; + version = 126.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8827764be0..7e9e6baf3f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "7656e94efcf4eedf1c16152c63f57fb52b6ad079", - "version" : "126.0.0" + "revision" : "d01c760dadbc2e987e7577e2476f95983dc6d38c", + "version" : "126.0.1" } }, { @@ -165,7 +165,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 19a07937ac..be59cf0606 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 380e8a7897..082551ae04 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 2211902822..a25a90e310 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 3353ef13dea81c55eea7d528de4174b0f016bf8b Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Tue, 19 Mar 2024 01:18:27 -0400 Subject: [PATCH 08/17] Handle subscription-related iOS use cases (#2427) Task/Issue URL: https://app.asana.com/0/414235014887631/1206844393131400/f --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a5457be403..1f07e8bad7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13764,7 +13764,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.0.1; + version = 126.1.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7e9e6baf3f..7a11e2163c 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "d01c760dadbc2e987e7577e2476f95983dc6d38c", - "version" : "126.0.1" + "revision" : "f4894b9c00dd7514c66d6b929c12315e0cd9c151", + "version" : "126.1.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index be59cf0606..51a9a0276c 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 082551ae04..85d865a47b 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index a25a90e310..90909e471e 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 7856a44f666de4db3d5e444253aa986d5ec0bacd Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 19 Mar 2024 19:13:31 +1100 Subject: [PATCH 09/17] Address bookmarks feedback (#2411) Task/Issue URL: https://app.asana.com/0/72649045549333/1206383230169466/f **Description**: This PR addresses feedback around bookmark editing and deleting. --- DuckDuckGo.xcodeproj/project.pbxproj | 234 +++++- .../AddBookmark.imageset/AddBookmark.svg | 8 + .../Images/AddBookmark.imageset/Contents.json | 2 +- .../icon-16-bookmark-add.pdf | Bin 3871 -> 0 bytes .../Images/AddFolder.imageset/AddFolder.svg | 8 + .../Images/AddFolder.imageset/Contents.json | 2 +- .../AddFolder.imageset/icon-16-folder-add.pdf | Bin 3057 -> 0 bytes .../BookmarksFolder.svg | 10 + .../BookmarksFolder.imageset/Contents.json | 12 + .../Chevron-Medium-Right-16.pdf} | Bin 1160 -> 1153 bytes .../Contents.json | 15 + .../Contents.json | 2 +- .../Images/Trash.imageset/Trash.svg | 9 + .../Bookmarks/Extensions/Bookmarks+Tab.swift | 41 + DuckDuckGo/Bookmarks/Model/Bookmark.swift | 80 +- .../Bookmarks/Model/BookmarkFolderInfo.swift | 32 + DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 29 +- .../Bookmarks/Model/BookmarkManager.swift | 25 + DuckDuckGo/Bookmarks/Model/BookmarkNode.swift | 21 +- .../Model/BookmarkOutlineViewDataSource.swift | 20 +- .../Model/BookmarkSidebarTreeController.swift | 10 +- .../Bookmarks/Model/PasteboardFolder.swift | 25 +- DuckDuckGo/Bookmarks/Model/PseudoFolder.swift | 2 +- .../Bookmarks/Services/BookmarkStore.swift | 1 + .../Services/BookmarkStoreMock.swift | 34 + .../Bookmarks/Services/ContextualMenu.swift | 225 ++++-- .../Services/LocalBookmarkStore.swift | 100 ++- .../Services/MenuItemSelectors.swift | 14 +- .../View/AddBookmarkFolderModalView.swift | 67 -- .../View/AddBookmarkFolderPopoverView.swift | 76 +- .../Bookmarks/View/AddBookmarkModalView.swift | 77 -- .../View/AddBookmarkPopoverView.swift | 103 +-- .../Bookmarks/View/BookmarkFolderPicker.swift | 2 +- .../View/BookmarkListViewController.swift | 110 ++- ...okmarkManagementDetailViewController.swift | 426 +++++----- ...kmarkManagementSidebarViewController.swift | 59 +- .../View/BookmarkOutlineCellView.swift | 95 ++- .../View/BookmarkTableCellView.swift | 252 +----- .../Bookmarks/View/BookmarkTableRowView.swift | 8 +- .../Dialog/AddEditBookmarkDialogView.swift | 123 +++ .../AddEditBookmarkFolderDialogView.swift | 107 +++ .../Dialog/AddEditBookmarkFolderView.swift | 132 ++++ .../View/Dialog/AddEditBookmarkView.swift | 113 +++ .../Dialog/BookmarkDialogButtonsView.swift | 186 +++++ .../Dialog/BookmarkDialogContainerView.swift | 50 ++ .../BookmarkDialogFolderManagementView.swift | 76 ++ .../BookmarkDialogStackedContentView.swift | 111 +++ .../View/Dialog/BookmarkFavoriteView.swift | 47 ++ .../Dialog/BookmarksDialogViewFactory.swift | 93 +++ .../AddBookmarkFolderModalViewModel.swift | 79 -- .../AddBookmarkFolderPopoverViewModel.swift | 23 +- .../ViewModel/AddBookmarkModalViewModel.swift | 127 --- .../AddBookmarkPopoverViewModel.swift | 31 +- ...itBookmarkDialogCoordinatorViewModel.swift | 74 ++ .../AddEditBookmarkDialogViewModel.swift | 216 +++++ ...AddEditBookmarkFolderDialogViewModel.swift | 181 +++++ .../ViewModel/BookmarksDialogViewModel.swift | 35 + .../View/BookmarksBarCollectionViewItem.swift | 108 +-- .../View/BookmarksBarMenuFactory.swift | 7 + .../View/BookmarksBarViewController.swift | 99 ++- .../View/BookmarksBarViewModel.swift | 16 +- .../Common/Extensions/NSMenuExtension.swift | 10 + DuckDuckGo/Common/Localizables/UserText.swift | 6 +- .../Model/HomePageFavoritesModel.swift | 15 +- DuckDuckGo/HomePage/View/FavoritesView.swift | 3 + .../View/HomePageViewController.swift | 19 +- DuckDuckGo/Localizable.xcstrings | 230 +----- .../VPNLocation/VPNLocationView.swift | 15 - .../{ => Dialogs}/Dialog.swift | 0 .../Dialogs/TieredDialogView.swift | 70 ++ .../TwoColumnsListView.swift | 62 ++ .../View+ConditionalModifiers.swift | 47 ++ .../Extensions/Bookmarks+TabTests.swift | 61 ++ .../BookmarksBarMenuFactoryTests.swift | 59 ++ .../Model/BaseBookmarkEntityTests.swift | 247 ++++++ .../Bookmarks/Model/BookmarkListTests.swift | 59 +- .../Bookmarks/Model/BookmarkNodeTests.swift | 170 ++++ .../BookmarkOutlineViewDataSourceTests.swift | 58 +- .../BookmarkSidebarTreeControllerTests.swift | 34 +- UnitTests/Bookmarks/Model/BookmarkTests.swift | 2 +- .../Bookmarks/Model/ContextualMenuTests.swift | 267 +++++++ .../Model/LocalBookmarkManagerTests.swift | 21 + .../Services/LocalBookmarkStoreTests.swift | 158 +++- ...kmarkDialogCoordinatorViewModelTests.swift | 170 ++++ .../AddEditBookmarkDialogViewModelTests.swift | 745 ++++++++++++++++++ ...itBookmarkFolderDialogViewModelTests.swift | 425 ++++++++++ .../BookmarksBarViewModelTests.swift | 214 +++++ .../HomePage/Mocks/MockBookmarkManager.swift | 4 + 88 files changed, 5840 insertions(+), 1601 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/BookmarksFolder.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json rename DuckDuckGo/Assets.xcassets/Images/{Chevron-Next-16.imageset/Chevron-Next-16.pdf => Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf} (53%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json rename DuckDuckGo/Assets.xcassets/Images/{Chevron-Next-16.imageset => Trash.imageset}/Contents.json (82%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg create mode 100644 DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift create mode 100644 DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift delete mode 100644 DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift delete mode 100644 DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift delete mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift delete mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift rename LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/{ => Dialogs}/Dialog.swift (100%) create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift create mode 100644 UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift create mode 100644 UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift create mode 100644 UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift create mode 100644 UnitTests/Bookmarks/Model/ContextualMenuTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1f07e8bad7..71f46f9bd0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -268,7 +268,6 @@ 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */; }; 3706FADD293F65D500E42796 /* AtbParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50382726A12400758A2B /* AtbParser.swift */; }; 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929426670D2A00AD2C21 /* BookmarkSidebarTreeController.swift */; }; 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -387,7 +386,6 @@ 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DF326B0002B00E14D75 /* SecureVaultLoginImporter.swift */; }; - 3706FB71293F65D500E42796 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C1DD4285C780C0089850C /* RecentlyClosedCoordinator.swift */; }; 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6197C5276B3168008396F0 /* FaviconHostReference.swift */; }; 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93A26B48ADF00879451 /* ASN1Parser.swift */; }; @@ -1279,8 +1277,6 @@ 4B9292CF2667123700AD2C21 /* BookmarkManagementSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */; }; 4B9292D02667123700AD2C21 /* BookmarkManagementSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */; }; 4B9292D12667123700AD2C21 /* BookmarkTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */; }; - 4B9292D22667123700AD2C21 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; - 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */; }; 4B9292D92667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */; }; @@ -1391,7 +1387,6 @@ 4B9579B52AC7AE700062CA31 /* FirefoxLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */; }; 4B9579B62AC7AE700062CA31 /* AtbParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50382726A12400758A2B /* AtbParser.swift */; }; 4B9579B72AC7AE700062CA31 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; - 4B9579B82AC7AE700062CA31 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; 4B9579B92AC7AE700062CA31 /* BookmarkSidebarTreeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929426670D2A00AD2C21 /* BookmarkSidebarTreeController.swift */; }; 4B9579BA2AC7AE700062CA31 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -1561,7 +1556,6 @@ 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DF326B0002B00E14D75 /* SecureVaultLoginImporter.swift */; }; 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */; }; - 4B957A6C2AC7AE700062CA31 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857E5AF42A79045800FC0FB4 /* PixelExperiment.swift */; }; 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */; }; @@ -2417,14 +2411,76 @@ 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DB6E7232AA0DC5800A17F3C /* LoginItems */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; + 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; + 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; 9F180D132B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; + 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D9A2B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; + 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; + 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; + 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; + 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F872DA52B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E02B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E12B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E52B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173E92B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = AA06B6B62672AF8100F541C5 /* Sparkle */; }; AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */; }; AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */; }; @@ -3052,15 +3108,9 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; - B6F9BDD82B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; - B6F9BDD92B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDDD2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDDE2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; - B6F9BDE12B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; - B6F9BDE22B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE62B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; @@ -3703,8 +3753,6 @@ 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementSidebarViewController.swift; sourceTree = ""; }; 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementSplitViewController.swift; sourceTree = ""; }; 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkTableRowView.swift; sourceTree = ""; }; - 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderModalView.swift; sourceTree = ""; }; - 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkModalView.swift; sourceTree = ""; }; 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListViewController.swift; sourceTree = ""; }; 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewController.swift; sourceTree = ""; }; 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerDataSource.swift; sourceTree = ""; }; @@ -4004,10 +4052,33 @@ 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; + 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; + 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; + 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; + 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; + 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; + 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; + 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; + 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderInfo.swift; sourceTree = ""; }; + 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; + 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; + 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; + 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; + 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; + 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; + 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; + 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactoryTests.swift; sourceTree = ""; }; + 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = ""; }; + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = ""; }; + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModel.swift; sourceTree = ""; }; AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariVersionReaderTests.swift; sourceTree = ""; }; AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProviderTests.swift; sourceTree = ""; }; AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionLoadingMock.swift; sourceTree = ""; }; @@ -4464,9 +4535,7 @@ B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; - B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBookmarkModalViewModel.swift; sourceTree = ""; }; B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfo.swift; sourceTree = ""; }; - B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderModalViewModel.swift; sourceTree = ""; }; B6F9BDE32B45CD1900677B33 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; @@ -5851,6 +5920,7 @@ children = ( 4B9292AE26670F5300AD2C21 /* NSOutlineViewExtensions.swift */, B6C0BB6629AEFF8100AE8E3C /* BookmarkExtension.swift */, + 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */, ); path = Extensions; sourceTree = ""; @@ -6674,6 +6744,49 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F872D9B2B9058B000138637 /* Extensions */ = { + isa = PBXGroup; + children = ( + 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 9F982F102B82264400231028 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */, + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 9FA173DD2B7A0ECE00EE4E6E /* Dialog */ = { + isa = PBXGroup; + children = ( + 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */, + 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */, + 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */, + 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */, + 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */, + 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, + 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, + 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, + 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */, + ); + path = Dialog; + sourceTree = ""; + }; + 9FA75A3C2BA00DF500DA5FA6 /* Factory */ = { + isa = PBXGroup; + children = ( + 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */, + ); + path = Factory; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -7021,6 +7134,9 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9F872D9B2B9058B000138637 /* Extensions */, + 9FA75A3C2BA00DF500DA5FA6 /* Factory */, + 9F982F102B82264400231028 /* ViewModels */, AA652CAE25DD8228009059CC /* Model */, AA652CAF25DD822C009059CC /* Services */, ); @@ -7042,6 +7158,8 @@ AA652CCD25DD9071009059CC /* BookmarkListTests.swift */, AA652CD225DDA6E9009059CC /* LocalBookmarkManagerTests.swift */, 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, + 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, + 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */, ); path = Model; sourceTree = ""; @@ -7430,8 +7548,10 @@ B69A14F12B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift */, B69A14F52B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift */, AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */, - B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */, - B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */, + 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */, + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -7502,9 +7622,8 @@ AAC5E4C125D6A6C3007F5990 /* View */ = { isa = PBXGroup; children = ( - 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */, + 9FA173DD2B7A0ECE00EE4E6E /* Dialog */, 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */, - 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */, AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */, 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */, B69A14F92B4D705D00B9417D /* BookmarkFolderPicker.swift */, @@ -7542,6 +7661,7 @@ AAC5E4CE25D6A709007F5990 /* BookmarkManager.swift */, 379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */, B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */, + 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */, ); path = Model; sourceTree = ""; @@ -9778,6 +9898,7 @@ 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 3706FA7B293F65D500E42796 /* FaviconUserScript.swift in Sources */, 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, + 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, @@ -9793,6 +9914,7 @@ 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */, 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */, 3706FA8C293F65D500E42796 /* AppStateChangedPublisher.swift in Sources */, + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 3706FA8D293F65D500E42796 /* BookmarkTableCellView.swift in Sources */, 3706FA8E293F65D500E42796 /* BookmarkManagementSidebarViewController.swift in Sources */, 3706FA8F293F65D500E42796 /* NSStackViewExtension.swift in Sources */, @@ -9843,6 +9965,7 @@ 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, + 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, 3706FABF293F65D500E42796 /* CrashReportReader.swift in Sources */, @@ -9884,7 +10007,6 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, @@ -9904,6 +10026,7 @@ 3706FAF1293F65D500E42796 /* PreferencesAboutView.swift in Sources */, 3706FAF2293F65D500E42796 /* ContentBlocking.swift in Sources */, 31F2D2002AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift in Sources */, + 9FA173E02B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, 3706FAF3293F65D500E42796 /* LocalAuthenticationService.swift in Sources */, 1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B6BCC5242AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, @@ -10045,7 +10168,6 @@ 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, - B6F9BDD92B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, 3706FB64293F65D500E42796 /* PixelDataModel.xcdatamodeld in Sources */, B626A75B29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */, @@ -10058,6 +10180,7 @@ 3706FB69293F65D500E42796 /* NavigationBarBadgeAnimationView.swift in Sources */, 1D1A334A2A6FEB170080ACED /* BurnerMode.swift in Sources */, B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, @@ -10066,7 +10189,6 @@ 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, - 3706FB71293F65D500E42796 /* AddBookmarkModalView.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, B69A14F32B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, @@ -10075,6 +10197,7 @@ 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */, + 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */, 987799F42999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, @@ -10134,7 +10257,6 @@ 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */, 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */, B6AFE6BD29A5D621002FF962 /* HTTPSUpgradeTabExtension.swift in Sources */, - B6F9BDE12B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */, 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */, 3706FB9C293F65D500E42796 /* TabCollectionViewModel.swift in Sources */, @@ -10147,6 +10269,7 @@ 3706FB9E293F65D500E42796 /* AboutModel.swift in Sources */, 3706FB9F293F65D500E42796 /* PasswordManagementCreditCardItemView.swift in Sources */, 3706FBA0293F65D500E42796 /* NSTextFieldExtension.swift in Sources */, + 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, @@ -10222,6 +10345,7 @@ 3706FBD9293F65D500E42796 /* NSAppearanceExtension.swift in Sources */, 3706FBDA293F65D500E42796 /* PermissionManager.swift in Sources */, 3706FBDB293F65D500E42796 /* DefaultBrowserPreferences.swift in Sources */, + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 3706FBDC293F65D500E42796 /* Permissions.xcdatamodeld in Sources */, 4B41EDB52B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */, 3706FBDD293F65D500E42796 /* PaddedImageButton.swift in Sources */, @@ -10236,6 +10360,7 @@ EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 3706FBE0293F65D500E42796 /* NSException+Catch.m in Sources */, 3706FBE1293F65D500E42796 /* AppStateRestorationManager.swift in Sources */, + 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 3706FBE2293F65D500E42796 /* ClickToLoadUserScript.swift in Sources */, 3706FBE3293F65D500E42796 /* WindowControllersManager.swift in Sources */, 37197EAA2942443D00394917 /* ModalSheetCancellable.swift in Sources */, @@ -10257,6 +10382,7 @@ 3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */, 3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */, 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */, @@ -10278,6 +10404,7 @@ 3706FC01293F65D500E42796 /* ChromiumBookmarksReader.swift in Sources */, 3706FC02293F65D500E42796 /* Downloads.xcdatamodeld in Sources */, B60C6F7829B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, + 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, 4B9754EC2984300100D7B834 /* EmailManagerExtension.swift in Sources */, @@ -10301,6 +10428,7 @@ 3706FC0C293F65D500E42796 /* NSAttributedStringExtension.swift in Sources */, C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, 3706FC0D293F65D500E42796 /* AnimationView.swift in Sources */, + 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 3706FC0E293F65D500E42796 /* NSRectExtension.swift in Sources */, 3706FC0F293F65D500E42796 /* YoutubeOverlayUserScript.swift in Sources */, 3775913729AB9A1C00E26367 /* SyncManagementDialogViewController.swift in Sources */, @@ -10385,12 +10513,14 @@ 3706FC50293F65D500E42796 /* FeedbackWindow.swift in Sources */, 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, + 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, B60293E72BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, 3706FC53293F65D500E42796 /* TabBarScrollView.swift in Sources */, 3706FC54293F65D500E42796 /* BookmarkListTreeControllerDataSource.swift in Sources */, 3706FC55293F65D500E42796 /* AddressBarViewController.swift in Sources */, 3706FC56293F65D500E42796 /* Permissions.swift in Sources */, + 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 3706FC57293F65D500E42796 /* TabPreviewWindowController.swift in Sources */, 3706FC58293F65D500E42796 /* NSSizeExtension.swift in Sources */, @@ -10415,6 +10545,7 @@ 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, 3706FC67293F65D500E42796 /* OperatingSystemVersionExtension.swift in Sources */, B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */, + 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, B677FC502B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, 3706FC68293F65D500E42796 /* ToggleableScrollView.swift in Sources */, 3706FC69293F65D500E42796 /* UserScripts.swift in Sources */, @@ -10432,6 +10563,7 @@ 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, 3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */, 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, + 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, @@ -10508,6 +10640,7 @@ 3706FDDE293F661700E42796 /* SuggestionViewModelTests.swift in Sources */, 3706FDDF293F661700E42796 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 3706FDE0293F661700E42796 /* TabIndexTests.swift in Sources */, + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 3706FDE1293F661700E42796 /* AdjacentItemEnumeratorTests.swift in Sources */, 3706FDE2293F661700E42796 /* PixelArgumentsTests.swift in Sources */, 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */, @@ -10536,6 +10669,7 @@ 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, 3706FDF9293F661700E42796 /* TabViewModelTests.swift in Sources */, + 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */, 3706FDFA293F661700E42796 /* DefaultBrowserPreferencesTests.swift in Sources */, 3706FDFB293F661700E42796 /* DispatchQueueExtensionsTests.swift in Sources */, 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, @@ -10599,6 +10733,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, @@ -10652,6 +10787,7 @@ 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, + 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, 56D145E929E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */, 3706FE4C293F661700E42796 /* CSVParserTests.swift in Sources */, 3706FE4D293F661700E42796 /* OnboardingTests.swift in Sources */, @@ -10665,6 +10801,7 @@ 3706FE54293F661700E42796 /* PasteboardBookmarkTests.swift in Sources */, 3706FE55293F661700E42796 /* CBRCompileTimeReporterTests.swift in Sources */, 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */, + 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 3706FE56293F661700E42796 /* FaviconManagerMock.swift in Sources */, 3706FE57293F661700E42796 /* LocalPinningManagerTests.swift in Sources */, 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, @@ -10688,6 +10825,7 @@ 3706FE64293F661700E42796 /* DownloadListStoreTests.swift in Sources */, 3706FE65293F661700E42796 /* ContentBlockingUpdatingTests.swift in Sources */, 3706FE67293F661700E42796 /* EncryptionMocks.swift in Sources */, + 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 3706FE68293F661700E42796 /* DuckPlayerURLExtensionTests.swift in Sources */, 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, @@ -10719,6 +10857,7 @@ 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, + 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B6CA4825298CE4B70067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, 3707C72D294B5D4100682A9F /* EmptyAttributionRulesProver.swift in Sources */, 376E2D2629428353001CD31B /* PrivacyReferenceTestHelper.swift in Sources */, @@ -10968,6 +11107,7 @@ 4B9579552AC7AE700062CA31 /* Logging.swift in Sources */, 4B9579562AC7AE700062CA31 /* CrashReportPromptPresenter.swift in Sources */, B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, + 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 4B9579572AC7AE700062CA31 /* BWCredential.swift in Sources */, 4B9579582AC7AE700062CA31 /* PreferencesRootView.swift in Sources */, 4B9579592AC7AE700062CA31 /* AppStateChangedPublisher.swift in Sources */, @@ -10977,6 +11117,7 @@ 4B95795D2AC7AE700062CA31 /* OptionalExtension.swift in Sources */, 4B95795E2AC7AE700062CA31 /* PasswordManagementLoginItemView.swift in Sources */, 4B95795F2AC7AE700062CA31 /* UserText.swift in Sources */, + 9F872D9A2B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 4B9579602AC7AE700062CA31 /* WKWebView+Download.swift in Sources */, 4B9579612AC7AE700062CA31 /* TabShadowConfig.swift in Sources */, 4B9579622AC7AE700062CA31 /* URLSessionExtension.swift in Sources */, @@ -10997,6 +11138,8 @@ 4B95796E2AC7AE700062CA31 /* LegacyBookmarkStore.swift in Sources */, 4B95796F2AC7AE700062CA31 /* NSAlert+DataImport.swift in Sources */, 4B9579702AC7AE700062CA31 /* MainWindow.swift in Sources */, + 9F872DA52B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 4B9579712AC7AE700062CA31 /* CrashReportPromptViewController.swift in Sources */, 4B9579722AC7AE700062CA31 /* BookmarksCleanupErrorHandling.swift in Sources */, 4B9579732AC7AE700062CA31 /* ContextMenuManager.swift in Sources */, @@ -11009,6 +11152,7 @@ 4B9579792AC7AE700062CA31 /* BWRequest.swift in Sources */, 4B95797A2AC7AE700062CA31 /* WKWebViewConfigurationExtensions.swift in Sources */, 4B95797B2AC7AE700062CA31 /* HomePageDefaultBrowserModel.swift in Sources */, + 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 4B95797C2AC7AE700062CA31 /* CrashReporter.swift in Sources */, 4B95797D2AC7AE700062CA31 /* AddressBarTextSelectionNavigation.swift in Sources */, 4B37EE7D2B4CFF8300A89A61 /* SurveyURLBuilder.swift in Sources */, @@ -11069,7 +11213,6 @@ 4B9579B52AC7AE700062CA31 /* FirefoxLoginReader.swift in Sources */, 4B9579B62AC7AE700062CA31 /* AtbParser.swift in Sources */, 4B9579B72AC7AE700062CA31 /* PreferencesDuckPlayerView.swift in Sources */, - 4B9579B82AC7AE700062CA31 /* AddBookmarkFolderModalView.swift in Sources */, 4B41EDB62B169883001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */, 4B9579B92AC7AE700062CA31 /* BookmarkSidebarTreeController.swift in Sources */, 4B9579BA2AC7AE700062CA31 /* HomePageFavoritesModel.swift in Sources */, @@ -11123,6 +11266,7 @@ 4B9579E42AC7AE700062CA31 /* PopUpWindow.swift in Sources */, 4B9579E52AC7AE700062CA31 /* Favicons.xcdatamodeld in Sources */, 4B9579E62AC7AE700062CA31 /* Publisher.asVoid.swift in Sources */, + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 4B9579E72AC7AE700062CA31 /* Waitlist.swift in Sources */, 3158B1582B0BF76000AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 4B9579E82AC7AE700062CA31 /* NavigationButtonMenuDelegate.swift in Sources */, @@ -11186,6 +11330,7 @@ 4B957A202AC7AE700062CA31 /* CancellableExtension.swift in Sources */, 4B957A212AC7AE700062CA31 /* PinnedTabsHostingView.swift in Sources */, 4B957A222AC7AE700062CA31 /* FirefoxBookmarksReader.swift in Sources */, + 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 4B0526622B1D55320054955A /* VPNFeedbackSender.swift in Sources */, 4B957A232AC7AE700062CA31 /* DeviceIdleStateDetector.swift in Sources */, 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, @@ -11209,6 +11354,7 @@ 4B957A332AC7AE700062CA31 /* Favicon.swift in Sources */, 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */, 4B957A342AC7AE700062CA31 /* SuggestionContainerViewModel.swift in Sources */, + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 4B957A352AC7AE700062CA31 /* FirePopoverWrapperViewController.swift in Sources */, 4B957A362AC7AE700062CA31 /* NSPasteboardItemExtension.swift in Sources */, 4B957A372AC7AE700062CA31 /* AutofillPreferencesModel.swift in Sources */, @@ -11246,6 +11392,7 @@ 4B957A542AC7AE700062CA31 /* VisitMenuItem.swift in Sources */, 4B957A552AC7AE700062CA31 /* EncryptionKeyStore.swift in Sources */, 4B957A562AC7AE700062CA31 /* TabExtensionsBuilder.swift in Sources */, + 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 1E2AE4C82ACB216B00684E0A /* HoverTrackingArea.swift in Sources */, 4B957A582AC7AE700062CA31 /* PasswordManagementIdentityItemView.swift in Sources */, 4B957A592AC7AE700062CA31 /* ProgressExtension.swift in Sources */, @@ -11270,7 +11417,6 @@ 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */, 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */, 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */, - 4B957A6C2AC7AE700062CA31 /* AddBookmarkModalView.swift in Sources */, 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */, 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */, 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */, @@ -11359,6 +11505,7 @@ 4B957AB62AC7AE700062CA31 /* FireproofDomains.xcdatamodeld in Sources */, 3158B14F2B0BF74F00AF130C /* DataBrokerProtectionManager.swift in Sources */, 4B957AB82AC7AE700062CA31 /* HomePageView.swift in Sources */, + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 4B957AB92AC7AE700062CA31 /* SerpHeadersNavigationResponder.swift in Sources */, 4B957ABA2AC7AE700062CA31 /* HomePageContinueSetUpModel.swift in Sources */, 4B957ABB2AC7AE700062CA31 /* WebKitDownloadTask.swift in Sources */, @@ -11375,12 +11522,12 @@ 4B957AC42AC7AE700062CA31 /* BWVault.swift in Sources */, 4B957AC52AC7AE700062CA31 /* NSViewExtension.swift in Sources */, BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, + 9FA173E52B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 4B957AC72AC7AE700062CA31 /* DownloadListViewModel.swift in Sources */, 4B957AC82AC7AE700062CA31 /* BookmarkManagementDetailViewController.swift in Sources */, 4B957AC92AC7AE700062CA31 /* CSVImporter.swift in Sources */, 4B957ACA2AC7AE700062CA31 /* StartupPreferences.swift in Sources */, 4B957ACB2AC7AE700062CA31 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, - B6F9BDE22B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 4B957ACC2AC7AE700062CA31 /* MainMenu.swift in Sources */, 4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */, 4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */, @@ -11395,6 +11542,7 @@ 4B957AD72AC7AE700062CA31 /* CustomRoundedCornersShape.swift in Sources */, 4B957AD82AC7AE700062CA31 /* LocaleExtension.swift in Sources */, 4B957AD92AC7AE700062CA31 /* SavePaymentMethodViewController.swift in Sources */, + 9FA173E92B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 4B957ADA2AC7AE700062CA31 /* BWStatus.swift in Sources */, 4B957ADB2AC7AE700062CA31 /* WebKitVersionProvider.swift in Sources */, B6BCC54D2AFDF24B002C5499 /* TaskWithProgress.swift in Sources */, @@ -11422,6 +11570,7 @@ 4B957AF02AC7AE700062CA31 /* NSException+Catch.m in Sources */, 4B957AF12AC7AE700062CA31 /* AppStateRestorationManager.swift in Sources */, 4B957AF22AC7AE700062CA31 /* DailyPixel.swift in Sources */, + 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 4B957AF32AC7AE700062CA31 /* NavigationHotkeyHandler.swift in Sources */, 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4B957AF42AC7AE700062CA31 /* ClickToLoadUserScript.swift in Sources */, @@ -11462,7 +11611,6 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, @@ -11548,6 +11696,7 @@ 4B957B612AC7AE700062CA31 /* HomePage.swift in Sources */, 4B957B622AC7AE700062CA31 /* RoundedSelectionRowView.swift in Sources */, B6A22B652B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9FA173E12B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, 4B957B632AC7AE700062CA31 /* LocalStatisticsStore.swift in Sources */, 4B957B642AC7AE700062CA31 /* BackForwardListItem.swift in Sources */, 4B957B672AC7AE700062CA31 /* AtbAndVariantCleanup.swift in Sources */, @@ -11623,6 +11772,8 @@ 4B957BA12AC7AE700062CA31 /* UserDefaults+NetworkProtectionShared.swift in Sources */, 4B957BA22AC7AE700062CA31 /* NavigationActionPolicyExtension.swift in Sources */, 4B957BA32AC7AE700062CA31 /* CIImageExtension.swift in Sources */, + 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, + 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 4B957BA42AC7AE700062CA31 /* NSMenuExtension.swift in Sources */, 4B957BA52AC7AE700062CA31 /* MainWindowController.swift in Sources */, 4B957BA62AC7AE700062CA31 /* Tab.swift in Sources */, @@ -11783,7 +11934,6 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, AAD6D8882696DF6D002393B3 /* CrashReportPromptViewController.swift in Sources */, @@ -11862,7 +12012,6 @@ 4B8AC93926B48A5100879451 /* FirefoxLoginReader.swift in Sources */, B69B503E2726A12500758A2B /* AtbParser.swift in Sources */, 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, - 4B9292D22667123700AD2C21 /* AddBookmarkFolderModalView.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 85589E8727BBB8F20038AD11 /* HomePageFavoritesModel.swift in Sources */, @@ -11871,6 +12020,7 @@ 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */, B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */, AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */, + 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */, 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */, AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, @@ -11897,6 +12047,7 @@ AA92127725ADA07900600CD4 /* WKWebViewExtension.swift in Sources */, AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */, B6C0B23626E732000031CB7F /* DownloadListItem.swift in Sources */, + 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, 4B9DB0232A983B24000927DB /* WaitlistRequest.swift in Sources */, B6B1E87E26D5DA0E0062C350 /* DownloadsPopover.swift in Sources */, 85774AFF2A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */, @@ -11968,6 +12119,7 @@ 85C6A29625CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift in Sources */, 85625998269C9C5F00EE44BC /* PasswordManagementPopover.swift in Sources */, 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */, + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 85589E9127BFB9810038AD11 /* HomePageRecentlyVisitedModel.swift in Sources */, 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */, B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, @@ -12053,9 +12205,9 @@ 4B6785472AA8DE68008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, + 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, B645D8F629FA95440024461F /* WKProcessPoolExtension.swift in Sources */, - 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */, 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */, 857E5AF52A79045800FC0FB4 /* PixelExperiment.swift in Sources */, B6C416A7294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift in Sources */, @@ -12082,6 +12234,7 @@ AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */, 370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, + 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* AddBookmarkPopover.swift in Sources */, 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, @@ -12090,6 +12243,7 @@ 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */, 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */, 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, B6BCC53B2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, @@ -12142,7 +12296,6 @@ AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, 4B4D60C02A0C848D00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, - B6F9BDD82B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, @@ -12155,6 +12308,7 @@ 1DB67F2D2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */, 4B44FEF32B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */, 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */, + 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, @@ -12188,6 +12342,7 @@ 4BE4005527CF3F19007D3161 /* SavePaymentMethodViewController.swift in Sources */, 1D2DC009290167A0008083A1 /* BWStatus.swift in Sources */, AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */, + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, B63D467A25BFC3E100874977 /* NSCoderExtensions.swift in Sources */, 1D2DC00B290167EC008083A1 /* RunningApplicationCheck.swift in Sources */, B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, @@ -12197,6 +12352,7 @@ 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, + 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */, 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */, B64C84F1269310120048FEBE /* PermissionManager.swift in Sources */, @@ -12263,6 +12419,7 @@ 4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */, 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, + 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */, @@ -12311,6 +12468,7 @@ 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */, 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, + 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 373A1AA8283ED1B900586521 /* BookmarkHTMLReader.swift in Sources */, B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */, @@ -12358,10 +12516,12 @@ AAC5E4E425D6BA9C007F5990 /* NSSizeExtension.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, + 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, C1E961EF2B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, @@ -12373,6 +12533,7 @@ 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, + 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 85308E25267FC9F2001ABD76 /* NSAlertExtension.swift in Sources */, B69A14F62B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift in Sources */, 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */, @@ -12454,6 +12615,8 @@ AA72D5FE25FFF94E00C77619 /* NSMenuItemExtension.swift in Sources */, 4BA1A6C2258B0A1300F6F690 /* ContiguousBytesExtension.swift in Sources */, B6A22B622B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, + 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, @@ -12492,6 +12655,7 @@ B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, + 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B630E7FE29C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 4B9292BB2667103100AD2C21 /* BookmarkNodeTests.swift in Sources */, 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */, @@ -12502,6 +12666,7 @@ B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */, 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */, AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */, + 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, B69B50462726C5C200758A2B /* AtbAndVariantCleanupTests.swift in Sources */, 567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, 4B59024C26B38BB800489384 /* ChromiumLoginReaderTests.swift in Sources */, @@ -12535,6 +12700,7 @@ 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, B63ED0DE26AFD9A300A9DAD1 /* AVCaptureDeviceMock.swift in Sources */, 98A95D88299A2DF900B9B81A /* BookmarkMigrationTests.swift in Sources */, B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */, @@ -12562,12 +12728,14 @@ B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */, 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, + 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, 4B9DB0562A983B55000927DB /* MockNotificationService.swift in Sources */, 4B02199C25E063DE00ED7DEA /* FireproofDomainsTests.swift in Sources */, AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, B60C6F8129B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, + 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */, @@ -12589,6 +12757,7 @@ B63ED0DC26AE7B1E00A9DAD1 /* WebViewMock.swift in Sources */, 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */, AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */, + 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 373A1AAA283ED86C00586521 /* BookmarksHTMLReaderTests.swift in Sources */, 317295D42AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, AA9C363025518CA9004B1BA3 /* FireTests.swift in Sources */, @@ -12638,6 +12807,7 @@ 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */, B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */, AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg new file mode 100644 index 0000000000..624b357638 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json index fd82163d37..b3519a0b69 100644 --- a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-16-bookmark-add.pdf", + "filename" : "AddBookmark.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf deleted file mode 100644 index 27e0c7b892a5446cbfb1350213f2e4b25779bb98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3871 zcmdT{TW{1x6n^)wm=`3d#O&NJ5{d)~L{*ER2@go94~t_$=w>(Fb*j)`-|vja$KGsO zklL48h>Sm<%Xcm_bN12OlUGl*j7_735}S`-8X=xN6N?uwHh1EKP*Rm=TIeuB)9P0! z;aQw@yUp#k?RLe)%e(&cykGX+@TSY3b_r8yuz7SO%D<|8e*3Zidee!;tIhVhVn285 z%l@-4;C5D_)9vQ=rr3H{q|5v8t*{uVoQuV~?Q*@lS#G;^`*rX8(NQ%u7rdXL=R(5=2*xX8BMk!z?hddy(Lob=AEHIya(h*WhJMAKn?2I>Az+Z}x7gk!O zvLRl;067KB1s#KeRLW5~k_sBh(LiEwCRrfx;J|?Nf-W&ybY!%~m3WNF8g!gg5v2%N zM5B^6BjT~JHo!Y4V2bWu}MpyG7&$d{8Tl%h|4z);&n znQC(|xM~vekkDu|<%AIFA*i?rVuF}T3)L!@r1s864bF^3B-K&FBuU{IBjDa!<)g?_ zc^7O1k`?kyxy;f;6Otz^p{FdQTu$0q7*J?`Yk;6_d`yJIsEjIl$vae!upAToWpsOC zpCno(qm5>WMX=~G@>iv%ZePtn-W5qw1EP%6hMLmId#rtE#kM}B+XTUlaOlg>5T6G_WCPFNf*Yv}`OXOZpdiw#hlRE!Pqfx+8ZV?~osLu1A;0vX(aBv4?> z@P9KI@%-@*6Oa2JPCUG~dGYOok42|x^17MU(+Zu=yNZ?ehMC$_Gi=hB&hiJ!PPwjS zw}z|k8KtV%A+gMz?{QO8d1zi7rp*UO-SgMW-EXr&m0tJe&}m%VUoB1rwOk!|_rD#y zEimamU)_9Oe%tg&_rL}Li(we%NdC^;II+XnxIe;Prs~L!Yir2RcTLpweqA87CmYKcXu3v8Zk{#=#Y92j$`R3JMbJ8YC diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg new file mode 100644 index 0000000000..b9b806e64d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json index b75b5d095f..55db1eefb9 100644 --- a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-16-folder-add.pdf", + "filename" : "AddFolder.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf deleted file mode 100644 index ba53443ad58c36c58052d6ce25d057f1946ee063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3057 zcmai0+in{-5Pj!Y@WntTAJp~6b|Fixq!ezyzGt}HS=tWT zg<;cYhlex6GqZDcb@AbiQAr3RwcP*qyO8qjTe-S?Jbk=AZBFC#pQ(R_jnpb@dgQxz z(~lT;H?&RIC;NXGcekfI=>V=_D2}JYxOot(i}A1fVO)QFFVC<0|AwRZQy5jbHBM=Z zH@xcKSzol3Ky1)C$l*b-*~`Uo*e$P(Q4X7om)#y?RZ2cZqBJSk;G}V?1g(vgRz++; zzzZ}8uc85*I&dq3ia9!yJ20vkoy&$G=S|L9CS_d634B)JP0CIVA}O0fDA;P1E-8lq zyAXZwUc%*KvNjG72ucAPLxUjZ<{+b6VSEkAgOO1aYD^T0R>|8Gv0|(!TO6GNN(O^M zI-HIYj2#$_38ijMF=uOKQ1H|ttFVf(BohEd@2$=gpEogt3^14CQXo6yoc2aWWvq7z zj(U|5h;x7(Uiw5^5r@yX8v>(tCX!vM>HvfjQbayFC?FePo3>7NNtNUad_oke=8CT2 zB{p5PF}Up=WqC0!EMFelKwoW|Dj&-bWaRU z`kg@5We-BYI#{n!s9x#pT|vf?lvwMeD+$KvYUb);a0?K5v98}uLW3Y{l^K(RFAy3! zhO^KfRA5W?7S~g2!ytuJ$kYrXL!id5GJA%uwRP(N!36VNR+j0t$buh^UsO2KndK9~s@VS-+$GXXQbG-qUhmeKR3 zc--tVMV-)(AP>eMo!JV-m{H*!(g+68VU2*9pzn!*&trtXL+vFmwhP7^o?w;V!NGP= zcXV@ykwG}>AQ|0=F%FC!>=2HJvCc1}e4z&=(VQmDH-5&nszzmTutS)?!&+9EiI3*< z7@>ID(Y)9$7%qOw^b@Q(*fLQY^kl^%;<_UKmQ}_$u;yT1!sqk4d+hw;>+3KsYMQ#* zNHx!2G-^%r8@?Hl$1?QiZmj!ux7(kN^3$&v^YE;G`TLJiuC6w>V*`F1Z|*nm_mA?^ zQn@U9!!lS_Gt+uJ?!O#{@hEAKTsOM&{q8jGa3W2IGkCQ*0F$9Xm=5O{2wG;)@9%Hm zs-y_kaz5vL{{?%^3;&NmIwpK0JEkej^UZ0q-QTvY6!h_HPCOpQ&jJ&In6yH_C8ie_ zB;fWKG&_1EX3O>@_(=~#_}IgXXOP8b`6-kz%07qiEt#RyVROG74}ymB?=FyzSI7Os s_*%ZYzj-|+WwqY#Pm%(z30_^_|1-k-I&^Oiry7pUE{L + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json new file mode 100644 index 0000000000..aee9d2b2fb --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "BookmarksFolder.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Chevron-Next-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf similarity index 53% rename from DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Chevron-Next-16.pdf rename to DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf index 463b781e2007af1affac63754707c0f9f2c7df0e..7d7e84fbe4807dd5ba892d914bf166abb2b50894 100644 GIT binary patch delta 511 zcmZ9Iu};H442DI7gp!TD!%3(M5|-_=@6KWcR>U))ZUyxXq7F!mRNZ+AUJzdzrAco| z@+Hpq|Nd_G?a!tqz~O|$Zgr~=v-)FTC^=ii!|z>>KqaDYR@qSof;due#|;Yi+wi~k5A2gL-Mz4zOEk8ThtRiyWxtM*sCs4HoHV6$0J_9 M-Eg;FKfk{G14id%#{d8T literal 1160 zcmZXUUvHZ*6vf}~r?@YX+DSDw1{qrgG^Kyp8*l zXD<18i|hOOQ&z)oAOYD9ypqe=xTy57D20(aR0gvnj60T-IxD~m8%eDtsz3z3iBy0` zh=jD-_ccSMb?o~%AuUW8%fRBaRSRv53Pa35iAYIfB(3V$1W|y8nZ_V-m`N-Y$o-js zoX)o*h-{!OQaR+&C=!y&4-K_+AVydf8!L&7zNGLwW<0U55G zu65XHMa&K2Do2H>Yg}e;gdiEPl3Ucfoufs=GkAgLcTm80;Mnw;7y`su>4FvdYK&Dd zQi_}Rz9`D3#)n6W9}Vxv-(L>BZMt_o`093fdRxBX1GlLQ(>PIPK+|kEDRFiAT4khkC=@gm<{c1`h;vTTqGZX4{~=l>kJZ$BDo)wJcrI)>Hi H!?(Nt>Sg|1 diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json new file mode 100644 index 0000000000..fa088142ba --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Chevron-Medium-Right-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json similarity index 82% rename from DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json index 715e36be33..ee8f9e1708 100644 --- a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Chevron-Next-16.pdf", + "filename" : "Trash.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg new file mode 100644 index 0000000000..385873ffbf --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift b/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift new file mode 100644 index 0000000000..82e5748c72 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift @@ -0,0 +1,41 @@ +// +// Bookmarks+Tab.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Tab { + + @MainActor + static func withContentOfBookmark(folder: BookmarkFolder, burnerMode: BurnerMode) -> [Tab] { + folder.children.compactMap { entity in + guard let url = (entity as? Bookmark)?.urlObject else { return nil } + return Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true, burnerMode: burnerMode) + } + } + +} + +extension TabCollection { + + @MainActor + static func withContentOfBookmark(folder: BookmarkFolder, burnerMode: BurnerMode) -> TabCollection { + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: burnerMode) + return TabCollection(tabs: tabs) + } + +} diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift index c7c5f61076..4d3cf399d8 100644 --- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift +++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift @@ -19,7 +19,7 @@ import Cocoa import Bookmarks -internal class BaseBookmarkEntity: Identifiable { +internal class BaseBookmarkEntity: Identifiable, Equatable, Hashable { static func singleEntity(with uuid: String) -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() @@ -99,6 +99,23 @@ internal class BaseBookmarkEntity: Identifiable { } } + // Subclasses needs to override to check equality on their properties + func isEqual(to instance: BaseBookmarkEntity) -> Bool { + id == instance.id && + title == instance.title && + isFolder == instance.isFolder + } + + static func == (lhs: BaseBookmarkEntity, rhs: BaseBookmarkEntity) -> Bool { + return type(of: lhs) == type(of: rhs) && lhs.isEqual(to: rhs) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + } + } final class BookmarkFolder: BaseBookmarkEntity { @@ -139,6 +156,38 @@ final class BookmarkFolder: BaseBookmarkEntity { super.init(id: id, title: title, isFolder: true) } + + override func isEqual(to instance: BaseBookmarkEntity) -> Bool { + guard let folder = instance as? BookmarkFolder else { + return false + } + return id == folder.id && + title == folder.title && + isFolder == folder.isFolder && + isParentFolderEqual(lhs: parentFolderUUID, rhs: folder.parentFolderUUID) && + children == folder.children + } + + override func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + hasher.combine(parentFolderUUID) + hasher.combine(children) + } + + // In some cases a bookmark folder that is child of the root folder has its `parentFolderUUID` set to `bookmarks_root`. In some other cases is nil. Making sure that comparing a `nil` and a `bookmarks_root` does not return false. Probably would be good idea to remove the optionality of `parentFolderUUID` in the future and set it to `bookmarks_root` when needed. + private func isParentFolderEqual(lhs: String?, rhs: String?) -> Bool { + switch (lhs, rhs) { + case (.none, .none): + return true + case (.some(let lhsValue), .some(let rhsValue)): + return lhsValue == rhsValue + case (.some(let value), .none), (.none, .some(let value)): + return value == "bookmarks_root" + } + } + } final class Bookmark: BaseBookmarkEntity { @@ -196,12 +245,33 @@ final class Bookmark: BaseBookmarkEntity { parentFolderUUID: bookmark.parentFolderUUID) } -} + convenience init(from bookmark: Bookmark, withNewUrl url: String, title: String, isFavorite: Bool) { + self.init(id: bookmark.id, + url: url, + title: title, + isFavorite: isFavorite, + parentFolderUUID: bookmark.parentFolderUUID) + } -extension BaseBookmarkEntity: Equatable { + override func isEqual(to instance: BaseBookmarkEntity) -> Bool { + guard let bookmark = instance as? Bookmark else { + return false + } + return id == bookmark.id && + title == bookmark.title && + isFolder == bookmark.isFolder && + url == bookmark.url && + isFavorite == bookmark.isFavorite && + parentFolderUUID == bookmark.parentFolderUUID + } - static func == (lhs: BaseBookmarkEntity, rhs: BaseBookmarkEntity) -> Bool { - return lhs.id == rhs.id + override func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + hasher.combine(url) + hasher.combine(isFavorite) + hasher.combine(parentFolderUUID) } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift b/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift new file mode 100644 index 0000000000..a4f8004f71 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift @@ -0,0 +1,32 @@ +// +// BookmarkFolderInfo.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol BookmarksEntityIdentifiable { + var entityId: String { get } + var parentId: String? { get } +} + +struct BookmarkEntityInfo: Equatable, BookmarksEntityIdentifiable { + let entity: BaseBookmarkEntity + let parent: BookmarkFolder? + + var entityId: String { entity.id } + var parentId: String? { parent?.id } +} diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 994557e2ba..dcae056327 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -125,6 +125,13 @@ struct BookmarkList { } } + mutating func update(bookmark: Bookmark, newURL: String, newTitle: String, newIsFavorite: Bool) -> Bookmark? { + guard !bookmark.isFolder else { return nil } + + let newBookmark = Bookmark(from: bookmark, withNewUrl: newURL, title: newTitle, isFavorite: newIsFavorite) + return updateBookmarkList(newBookmark: newBookmark, oldBookmark: bookmark) + } + mutating func updateUrl(of bookmark: Bookmark, to newURL: String) -> Bookmark? { guard !bookmark.isFolder else { return nil } @@ -132,12 +139,25 @@ struct BookmarkList { os_log("BookmarkList: Update failed, new url already in bookmark list") return nil } + + let newBookmark = Bookmark(from: bookmark, with: newURL) + return updateBookmarkList(newBookmark: newBookmark, oldBookmark: bookmark) + } + + func bookmarks() -> [IdentifiableBookmark] { + return allBookmarkURLsOrdered + } + +} + +private extension BookmarkList { + + mutating private func updateBookmarkList(newBookmark: Bookmark, oldBookmark bookmark: Bookmark) -> Bookmark? { guard itemsDict[bookmark.url] != nil, let index = allBookmarkURLsOrdered.firstIndex(of: IdentifiableBookmark(from: bookmark)) else { os_log("BookmarkList: Update failed, no such item in bookmark list") return nil } - let newBookmark = Bookmark(from: bookmark, with: newURL) let newIdentifiableBookmark = IdentifiableBookmark(from: newBookmark) allBookmarkURLsOrdered.remove(at: index) @@ -147,13 +167,8 @@ struct BookmarkList { let updatedBookmarks = existingBookmarks.filter { $0.id != bookmark.id } itemsDict[bookmark.url] = updatedBookmarks - itemsDict[newURL] = (itemsDict[newURL] ?? []) + [bookmark] + itemsDict[newBookmark.url] = (itemsDict[newBookmark.url] ?? []) + [newBookmark] return newBookmark } - - func bookmarks() -> [IdentifiableBookmark] { - return allBookmarkURLsOrdered - } - } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index ee19960da1..b3172da78e 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -35,7 +35,9 @@ protocol BookmarkManager: AnyObject { func remove(folder: BookmarkFolder) func remove(objectsWithUUIDs uuids: [String]) func update(bookmark: Bookmark) + func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) func update(folder: BookmarkFolder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) @discardableResult func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? func add(bookmark: Bookmark, to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) func add(objectsWithUUIDs uuids: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) @@ -211,12 +213,35 @@ final class LocalBookmarkManager: BookmarkManager { } + func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) { + guard list != nil else { return } + guard getBookmark(forUrl: bookmark.url) != nil else { + os_log("LocalBookmarkManager: Failed to update bookmark url - not in the list.", type: .error) + return + } + + guard let newBookmark = list?.update(bookmark: bookmark, newURL: url.absoluteString, newTitle: title, newIsFavorite: isFavorite) else { + os_log("LocalBookmarkManager: Failed to update URL of bookmark.", type: .error) + return + } + + bookmarkStore.update(bookmark: newBookmark) + loadBookmarks() + requestSync() + } + func update(folder: BookmarkFolder) { bookmarkStore.update(folder: folder) loadBookmarks() requestSync() } + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + bookmarkStore.update(folder: folder, andMoveToParent: parent) + loadBookmarks() + requestSync() + } + func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? { guard list != nil else { return nil } guard getBookmark(forUrl: bookmark.url) != nil else { diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift b/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift index 86f24bdc28..4f6b58c10f 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift @@ -67,11 +67,24 @@ final class BookmarkNode: Hashable { return 0 } - init(representedObject: AnyObject, parent: BookmarkNode?) { + /// Creates an instance of a bookmark node. + /// - Parameters: + /// - representedObject: The represented object contained in the node. + /// - parent: An optional parent node. + /// - uniqueId: A unique identifier for the node. This should be used only in unit tests. + /// - Attention: Use this initializer only in tests. + init(representedObject: AnyObject, parent: BookmarkNode?, uniqueId: Int) { self.representedObject = representedObject self.parent = parent - self.uniqueID = BookmarkNode.incrementingID + self.uniqueID = uniqueId + } + /// Creates an instance of a bookmark node. + /// - Parameters: + /// - representedObject: The represented object contained in the node. + /// - parent: An optional parent node. + convenience init(representedObject: AnyObject, parent: BookmarkNode?) { + self.init(representedObject: representedObject, parent: parent, uniqueId: BookmarkNode.incrementingID) BookmarkNode.incrementingID += 1 } @@ -165,7 +178,7 @@ final class BookmarkNode: Hashable { // The Node class will most frequently represent Bookmark entities and PseudoFolders. Because of this, their unique properties are // used to derive the hash for the node so that equality can be handled based on the represented object. if let entity = self.representedObject as? BaseBookmarkEntity { - hasher.combine(entity.id) + hasher.combine(entity.hashValue) } else if let folder = self.representedObject as? PseudoFolder { hasher.combine(folder.name) } else { @@ -176,7 +189,7 @@ final class BookmarkNode: Hashable { // MARK: - Equatable class func == (lhs: BookmarkNode, rhs: BookmarkNode) -> Bool { - return lhs === rhs + return lhs.uniqueID == rhs.uniqueID && lhs.representedObjectEquals(rhs.representedObject) } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index edfcfa6e2e..93a404fa95 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -30,10 +30,12 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS @Published var selectedFolders: [BookmarkFolder] = [] let treeController: BookmarkTreeController - var expandedNodesIDs = Set() + private(set) var expandedNodesIDs = Set() private let contentMode: ContentMode private let bookmarkManager: BookmarkManager + private let showMenuButtonOnHover: Bool + private let onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? private let presentFaviconsFetcherOnboarding: (() -> Void)? private var favoritesPseudoFolder = PseudoFolder.favorites @@ -43,11 +45,15 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS contentMode: ContentMode, bookmarkManager: BookmarkManager, treeController: BookmarkTreeController, + showMenuButtonOnHover: Bool = true, + onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? = nil, presentFaviconsFetcherOnboarding: (() -> Void)? = nil ) { self.contentMode = contentMode self.bookmarkManager = bookmarkManager self.treeController = treeController + self.showMenuButtonOnHover = showMenuButtonOnHover + self.onMenuRequestedAction = onMenuRequestedAction self.presentFaviconsFetcherOnboarding = presentFaviconsFetcherOnboarding super.init() @@ -123,6 +129,8 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } let cell = outlineView.makeView(withIdentifier: .init(BookmarkOutlineCellView.className()), owner: self) as? BookmarkOutlineCellView ?? BookmarkOutlineCellView(identifier: .init(BookmarkOutlineCellView.className())) + cell.shouldShowMenuButton = showMenuButtonOnHover + cell.delegate = self if let bookmark = node.representedObject as? Bookmark { cell.update(from: bookmark) @@ -233,7 +241,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS // Folders cannot be dragged onto any of their descendants: let containsDescendantOfDestination = draggedFolders.contains { draggedFolder in - let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name) + let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name, parentFolderUUID: draggedFolder.parentFolderUUID, children: draggedFolder.children) guard let draggedNode = treeController.node(representing: folder) else { return false @@ -329,3 +337,11 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } } + +// MARK: - BookmarkOutlineCellViewDelegate + +extension BookmarkOutlineViewDataSource: BookmarkOutlineCellViewDelegate { + func outlineCellViewRequestedMenu(_ cell: BookmarkOutlineCellView) { + onMenuRequestedAction?(cell) + } +} diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift b/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift index 06167f2356..b8549cad89 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift @@ -33,19 +33,11 @@ final class BookmarkSidebarTreeController: BookmarkTreeControllerDataSource { // MARK: - Private private func childNodesForRootNode(_ node: BookmarkNode) -> [BookmarkNode] { - let favorites = PseudoFolder.favorites - let favoritesNode = BookmarkNode(representedObject: favorites, parent: node) - favoritesNode.canHaveChildNodes = false - - let blankSpacer = SpacerNode.blank - let spacerNode = BookmarkNode(representedObject: blankSpacer, parent: node) - spacerNode.canHaveChildNodes = false - let bookmarks = PseudoFolder.bookmarks let bookmarksNode = BookmarkNode(representedObject: bookmarks, parent: node) bookmarksNode.canHaveChildNodes = true - return [favoritesNode, spacerNode, bookmarksNode] + return [bookmarksNode] } private func childNodes(for parentNode: BookmarkNode) -> [BookmarkNode] { diff --git a/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift b/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift index ffc3eb9a41..f99f1e040d 100644 --- a/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift +++ b/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift @@ -26,12 +26,15 @@ struct PasteboardFolder: Hashable { static let name = "name" } - let id: String - let name: String + var id: String { folder.id } + var name: String { folder.title } + var parentFolderUUID: String? { folder.parentFolderUUID } + var children: [BaseBookmarkEntity] { folder.children } - init(id: String, name: String) { - self.id = id - self.name = name + private let folder: BookmarkFolder + + init(folder: BookmarkFolder) { + self.folder = folder } // MARK: - Pasteboard Restoration @@ -41,7 +44,7 @@ struct PasteboardFolder: Hashable { return nil } - self.init(id: id, name: name) + self.init(folder: .init(id: id, title: name)) } init?(pasteboardItem: NSPasteboardItem) { @@ -78,19 +81,17 @@ struct PasteboardFolder: Hashable { static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal) var pasteboardFolder: PasteboardFolder { - return PasteboardFolder(id: folderID, name: folderName) + return PasteboardFolder(folder: folder) } var internalDictionary: PasteboardAttributes { return pasteboardFolder.internalDictionaryRepresentation } - private let folderID: String - private let folderName: String + private let folder: BookmarkFolder init(folder: BookmarkFolder) { - self.folderID = folder.id - self.folderName = folder.title + self.folder = folder } // MARK: - NSPasteboardWriting @@ -102,7 +103,7 @@ struct PasteboardFolder: Hashable { func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { switch type { case .string: - return folderName + return folder.title case FolderPasteboardWriter.folderUTIInternalType: return internalDictionary default: diff --git a/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift b/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift index c3aed14be7..85cef1888a 100644 --- a/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift +++ b/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift @@ -22,7 +22,7 @@ import Foundation final class PseudoFolder: Equatable { static let favorites = PseudoFolder(id: UUID().uuidString, name: UserText.favorites, icon: .favoriteFilledBorder) - static let bookmarks = PseudoFolder(id: UUID().uuidString, name: UserText.bookmarks, icon: .folder) + static let bookmarks = PseudoFolder(id: UUID().uuidString, name: UserText.bookmarks, icon: .bookmarksFolder) let id: String let name: String diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 6b7981c96a..3466d1a7fa 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -52,6 +52,7 @@ protocol BookmarkStore { func remove(objectsWithUUIDs: [String], completion: @escaping (Bool, Error?) -> Void) func update(bookmark: Bookmark) func update(folder: BookmarkFolder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) func add(objectsWithUUIDs: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) func update(objectsWithUUIDs uuids: [String], update: @escaping (BaseBookmarkEntity) -> Void, completion: @escaping (Error?) -> Void) func canMoveObjectWithUUID(objectUUID uuid: String, to parent: BookmarkFolder) -> Bool diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index f505891891..2ab57158a9 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -46,6 +46,11 @@ public final class BookmarkStoreMock: BookmarkStore { self.updateFavoriteIndexCalled = updateFavoriteIndexCalled } + var capturedFolder: BookmarkFolder? + var capturedParentFolder: BookmarkFolder? + var capturedParentFolderType: ParentFolderType? + var capturedBookmark: Bookmark? + var loadAllCalled = false var bookmarks: [BaseBookmarkEntity]? var loadError: Error? @@ -60,6 +65,8 @@ public final class BookmarkStoreMock: BookmarkStore { func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void) { saveBookmarkCalled = true bookmarks?.append(bookmark) + capturedParentFolder = parent + capturedBookmark = bookmark completion(saveBookmarkSuccess, saveBookmarkError) } @@ -68,6 +75,8 @@ public final class BookmarkStoreMock: BookmarkStore { var saveFolderError: Error? func save(folder: BookmarkFolder, parent: BookmarkFolder?, completion: @escaping (Bool, Error?) -> Void) { saveFolderCalled = true + capturedFolder = folder + capturedParentFolder = parent completion(saveFolderSuccess, saveFolderError) } @@ -92,11 +101,20 @@ public final class BookmarkStoreMock: BookmarkStore { } updateBookmarkCalled = true + capturedBookmark = bookmark } var updateFolderCalled = false func update(folder: BookmarkFolder) { updateFolderCalled = true + capturedFolder = folder + } + + var updateFolderAndMoveToParentCalled = false + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + updateFolderAndMoveToParentCalled = true + capturedFolder = folder + capturedParentFolderType = parent } var addChildCalled = false @@ -122,8 +140,11 @@ public final class BookmarkStoreMock: BookmarkStore { } var moveObjectUUIDCalled = false + var capturedObjectUUIDs: [String]? func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) { moveObjectUUIDCalled = true + capturedObjectUUIDs = objectUUIDs + capturedParentFolderType = withinParentFolder } var updateFavoriteIndexCalled = false @@ -135,4 +156,17 @@ public final class BookmarkStoreMock: BookmarkStore { func handleFavoritesAfterDisablingSync() {} } +extension ParentFolderType: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.root, .root): + return true + case (.parent(let lhsValue), .parent(let rhsValue)): + return lhsValue == rhsValue + default: + return false + } + } +} + #endif diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index 49f694be2b..f79e265d96 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -18,10 +18,19 @@ import AppKit -struct ContextualMenu { +enum ContextualMenu { + + static func menu(for objects: [Any]?) -> NSMenu? { + menu(for: objects, target: nil) + } + + /// Creates an instance of NSMenu for the specified Objects and target. + /// - Parameters: + /// - objects: The objects to create the menu for. + /// - target: The target to associate to the `NSMenuItem` + /// - Returns: An instance of NSMenu or nil if `objects` is not a `Bookmark` or a `Folder`. + static func menu(for objects: [Any]?, target: AnyObject?) -> NSMenu? { - // Not all contexts support an editing option for bookmarks. The option is displayed by default, but `includeBookmarkEditMenu` can disable it. - static func menu(for objects: [Any]?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { guard let objects = objects, objects.count > 0 else { return menuForNoSelection() } @@ -31,140 +40,183 @@ struct ContextualMenu { } let node = objects.first as? BookmarkNode - let object = node?.representedObject ?? objects.first as? BaseBookmarkEntity + let object = node?.representedObject as? BaseBookmarkEntity ?? objects.first as? BaseBookmarkEntity + let parentFolder = node?.parent?.representedObject as? BookmarkFolder - if let bookmark = object as? Bookmark { - return menu(for: bookmark, includeBookmarkEditMenu: includeBookmarkEditMenu) - } else if let folder = object as? BookmarkFolder { - return menu(for: folder) - } else { - return nil - } - } + guard let object else { return nil } - // MARK: - Single Item Menu Creation + let menu = menu(for: object, parentFolder: parentFolder) - private static func menuForNoSelection() -> NSMenu { - let menu = NSMenu(title: "") - menu.addItem(newFolderMenuItem()) + menu?.items.forEach { item in + item.target = target + } return menu } - private static func menu(for bookmark: Bookmark, includeBookmarkEditMenu: Bool) -> NSMenu { - let menu = NSMenu(title: "") + /// Creates an instance of NSMenu for the specified `BaseBookmarkEntity`and parent `BookmarkFolder`. + /// + /// - Parameters: + /// - entity: The bookmark entity to create the menu for. + /// - parentFolder: An optional `BookmarkFolder`. + /// - Returns: An instance of NSMenu or nil if `entity` is not a `Bookmark` or a `Folder`. + static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?) -> NSMenu? { + let menu: NSMenu? + if let bookmark = entity as? Bookmark { + menu = self.menu(for: bookmark, parent: parentFolder, isFavorite: bookmark.isFavorite) + } else if let folder = entity as? BookmarkFolder { + // When the user edits a folder we need to show the parent in the folder picker. Folders directly child of PseudoFolder `Bookmarks` have nil parent because their parent is not an instance of `BookmarkFolder` + menu = self.menu(for: folder, parent: parentFolder) + } else { + menu = nil + } - menu.addItem(openBookmarkInNewTabMenuItem(bookmark: bookmark)) - menu.addItem(openBookmarkInNewWindowMenuItem(bookmark: bookmark)) - menu.addItem(NSMenuItem.separator()) + return menu + } - menu.addItem(addBookmarkToFavoritesMenuItem(bookmark: bookmark)) + /// Returns an array of `NSMenuItem` to show for a bookmark. + /// + /// - Important: The `representedObject` for the `NSMenuItem` returned is `nil`. This function is meant to be used for scenarios where the model is not available at the time of creating the `NSMenu` such as from the BookmarkBarCollectionViewItem. + /// + /// - Parameter isFavorite: True if the menu item should contain a menu item to add to favorites. False to contain a menu item to remove from favorites. + /// - Returns: An array of `NSMenuItem` + static func bookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { + menuItems(for: nil, parent: nil, isFavorite: isFavorite) + } - if includeBookmarkEditMenu { - menu.addItem(editBookmarkMenuItem(bookmark: bookmark)) - } + /// Returns an array of `NSMenuItem` to show for a bookmark folder. + /// + /// - Important: The `representedObject` for the `NSMenuItem` returned is `nil`. This function is meant to be used for scenarios where the model is not available at the time of creating the `NSMenu` such as from the BookmarkBarCollectionViewItem. + /// + /// - Returns: An array of `NSMenuItem` + static func folderMenuItems() -> [NSMenuItem] { + menuItems(for: nil, parent: nil) + } - menu.addItem(NSMenuItem.separator()) +} - menu.addItem(copyBookmarkMenuItem(bookmark: bookmark)) - menu.addItem(deleteBookmarkMenuItem(bookmark: bookmark)) - menu.addItem(NSMenuItem.separator()) +private extension ContextualMenu { - menu.addItem(newFolderMenuItem()) + static func menuForNoSelection() -> NSMenu { + NSMenu(items: [addFolderMenuItem(folder: nil)]) + } - return menu + static func menu(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> NSMenu { + NSMenu(items: menuItems(for: bookmark, parent: parent, isFavorite: isFavorite)) } - private static func menu(for folder: BookmarkFolder) -> NSMenu { - let menu = NSMenu(title: "") + static func menu(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenu { + NSMenu(items: menuItems(for: folder, parent: parent)) + } - menu.addItem(renameFolderMenuItem(folder: folder)) - menu.addItem(deleteFolderMenuItem(folder: folder)) - menu.addItem(NSMenuItem.separator()) + static func menuItems(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> [NSMenuItem] { + [ + openBookmarkInNewTabMenuItem(bookmark: bookmark), + openBookmarkInNewWindowMenuItem(bookmark: bookmark), + NSMenuItem.separator(), + addBookmarkToFavoritesMenuItem(isFavorite: isFavorite, bookmark: bookmark), + NSMenuItem.separator(), + editBookmarkMenuItem(bookmark: bookmark), + copyBookmarkMenuItem(bookmark: bookmark), + deleteBookmarkMenuItem(bookmark: bookmark), + moveToEndMenuItem(entity: bookmark, parent: parent), + NSMenuItem.separator(), + addFolderMenuItem(folder: parent), + manageBookmarksMenuItem(), + ] + } - menu.addItem(openInNewTabsMenuItem(folder: folder)) + static func menuItems(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> [NSMenuItem] { + [ + openInNewTabsMenuItem(folder: folder), + openAllInNewWindowMenuItem(folder: folder), + NSMenuItem.separator(), + editFolderMenuItem(folder: folder, parent: parent), + deleteFolderMenuItem(folder: folder), + moveToEndMenuItem(entity: folder, parent: parent), + NSMenuItem.separator(), + addFolderMenuItem(folder: folder), + manageBookmarksMenuItem(), + ] + } - return menu + static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.representedObject = representedObject + return item } - // MARK: - Menu Items + // MARK: - Single Bookmark Menu Items - static func newFolderMenuItem() -> NSMenuItem { - return menuItem(UserText.newFolder, #selector(FolderMenuItemSelectors.newFolder(_:))) + static func openBookmarkInNewTabMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.openInNewTab, #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), bookmark) } - static func renameFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.renameFolder, #selector(FolderMenuItemSelectors.renameFolder(_:)), folder) + static func openBookmarkInNewWindowMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.openInNewWindow, #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), bookmark) } - static func deleteFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.deleteFolder, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) + static func manageBookmarksMenuItem() -> NSMenuItem { + menuItem(UserText.bookmarksManageBookmarks, #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) } - static func openInNewTabsMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) + static func addBookmarkToFavoritesMenuItem(isFavorite: Bool, bookmark: Bookmark?) -> NSMenuItem { + let title = isFavorite ? UserText.removeFromFavorites : UserText.addToFavorites + return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) } - static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { - return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) + static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { + let title = allFavorites ? UserText.removeFromFavorites : UserText.addToFavorites + return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) } - static func openBookmarkInNewTabMenuItem(bookmark: Bookmark) -> NSMenuItem { - return menuItem(UserText.openInNewTab, #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), bookmark) + static func editBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.editBookmark, #selector(BookmarkMenuItemSelectors.editBookmark(_:)), bookmark) } - static func openBookmarkInNewWindowMenuItem(bookmark: Bookmark) -> NSMenuItem { - return menuItem(UserText.openInNewWindow, #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), bookmark) + static func copyBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.copy, #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), bookmark) } - static func addBookmarkToFavoritesMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title: String - - if bookmark.isFavorite { - title = UserText.removeFromFavorites - } else { - title = UserText.addToFavorites - } - - return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) + static func deleteBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.bookmarksBarContextMenuDelete, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) } - static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { - let title: String + static func moveToEndMenuItem(entity: BaseBookmarkEntity?, parent: BookmarkFolder?) -> NSMenuItem { + let bookmarkEntityInfo = entity.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) } + return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), bookmarkEntityInfo) + } - if allFavorites { - title = UserText.removeFromFavorites - } else { - title = UserText.addToFavorites - } + // MARK: - Bookmark Folder Menu Items - return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) + static func openInNewTabsMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.openAllInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) } - static func editBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Edit…", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.editBookmark(_:)), bookmark) + static func openAllInNewWindowMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.openAllTabsInNewWindow, #selector(FolderMenuItemSelectors.openAllInNewWindow(_:)), folder) } - static func copyBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Copy", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), bookmark) + static func addFolderMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.addFolder, #selector(FolderMenuItemSelectors.newFolder(_:)), folder) } - static func deleteBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Delete", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) + static func editFolderMenuItem(folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenuItem { + let folderEntityInfo = folder.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) } + return menuItem(UserText.editBookmark, #selector(FolderMenuItemSelectors.editFolder(_:)), folderEntityInfo) } - static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem { - let item = NSMenuItem(title: title, action: action, keyEquivalent: "") - item.representedObject = representedObject - return item + static func deleteFolderMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.bookmarksBarContextMenuDelete, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) } // MARK: - Multi-Item Menu Creation - private static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { + static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { + menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) + } + + static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { let menu = NSMenu(title: "") var menuItems: [NSMenuItem] = [] @@ -185,8 +237,7 @@ struct ContextualMenu { menuItems.append(NSMenuItem.separator()) } - let title = NSLocalizedString("Delete", comment: "Command") - let deleteItem = NSMenuItem(title: title, action: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), keyEquivalent: "") + let deleteItem = NSMenuItem(title: UserText.bookmarksBarContextMenuDelete, action: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), keyEquivalent: "") deleteItem.representedObject = entities menuItems.append(deleteItem) diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index 307d16cc5d..e2be507a44 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -391,18 +391,27 @@ final class LocalBookmarkStore: BookmarkStore { } func update(folder: BookmarkFolder) { - do { - _ = try applyChangesAndSave(changes: { context in - let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: folder.id) - let folderFetchRequestResults = try? context.fetch(folderFetchRequest) - - guard let bookmarkFolderMO = folderFetchRequestResults?.first else { - assertionFailure("LocalBookmarkStore: Failed to get BookmarkEntity from the context") - throw BookmarkStoreError.missingEntity + _ = try applyChangesAndSave(changes: { [weak self] context in + guard let self = self else { + throw BookmarkStoreError.storeDeallocated } + try update(folder: folder, in: context) + }) + } catch { + let error = error as NSError + commonOnSaveErrorHandler(error) + } + } - bookmarkFolderMO.update(with: folder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + do { + _ = try applyChangesAndSave(changes: { [weak self] context in + guard let self = self else { + throw BookmarkStoreError.storeDeallocated + } + let folderEntity = try update(folder: folder, in: context) + try move(entities: [folderEntity], toIndex: nil, withinParentFolderType: parent, in: context) }) } catch { let error = error as NSError @@ -566,10 +575,6 @@ final class LocalBookmarkStore: BookmarkStore { throw BookmarkStoreError.storeDeallocated } - guard let rootFolder = self.bookmarksRoot(in: context) else { - throw BookmarkStoreError.missingRoot - } - // Guarantee that bookmarks are fetched in the same order as the UUIDs. In the future, this should fetch all objects at once with a // batch fetch request and have them sorted in the correct order. let bookmarkManagedObjects: [BookmarkEntity] = objectUUIDs.compactMap { uuid in @@ -577,28 +582,8 @@ final class LocalBookmarkStore: BookmarkStore { return (try? context.fetch(entityFetchRequest))?.first } - let newParentFolder: BookmarkEntity - - switch type { - case .root: newParentFolder = rootFolder - case .parent(let newParentUUID): - let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + try move(entities: bookmarkManagedObjects, toIndex: index, withinParentFolderType: type, in: context) - if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { - newParentFolder = fetchedParent - } else { - throw BookmarkStoreError.missingEntity - } - } - - if let index = index, index < newParentFolder.childrenArray.count { - self.move(entities: bookmarkManagedObjects, to: index, within: newParentFolder) - } else { - for bookmarkManagedObject in bookmarkManagedObjects { - bookmarkManagedObject.parent = nil - newParentFolder.addToChildren(bookmarkManagedObject) - } - } }, onError: { [weak self] error in self?.commonOnSaveErrorHandler(error) DispatchQueue.main.async { completion(error) } @@ -996,6 +981,53 @@ final class LocalBookmarkStore: BookmarkStore { } +private extension LocalBookmarkStore { + + @discardableResult + func update(folder: BookmarkFolder, in context: NSManagedObjectContext) throws -> BookmarkEntity { + let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: folder.id) + let folderFetchRequestResults = try? context.fetch(folderFetchRequest) + + guard let bookmarkFolderMO = folderFetchRequestResults?.first else { + assertionFailure("LocalBookmarkStore: Failed to get BookmarkEntity from the context") + throw BookmarkStoreError.missingEntity + } + + bookmarkFolderMO.update(with: folder) + return bookmarkFolderMO + } + + func move(entities: [BookmarkEntity], toIndex index: Int?, withinParentFolderType type: ParentFolderType, in context: NSManagedObjectContext) throws { + guard let rootFolder = bookmarksRoot(in: context) else { + throw BookmarkStoreError.missingRoot + } + + let newParentFolder: BookmarkEntity + + switch type { + case .root: newParentFolder = rootFolder + case .parent(let newParentUUID): + let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + + if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { + newParentFolder = fetchedParent + } else { + throw BookmarkStoreError.missingEntity + } + } + + if let index = index, index < newParentFolder.childrenArray.count { + self.move(entities: entities, to: index, within: newParentFolder) + } else { + for bookmarkManagedObject in entities { + bookmarkManagedObject.parent = nil + newParentFolder.addToChildren(bookmarkManagedObject) + } + } + } + +} + extension LocalBookmarkStore.BookmarkStoreError: CustomNSError { var errorCode: Int { diff --git a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift index 44114fc9d8..393b22de8e 100644 --- a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift +++ b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift @@ -18,7 +18,13 @@ import AppKit -@objc protocol BookmarkMenuItemSelectors { +@objc protocol BookmarksMenuItemSelectors { + func newFolder(_ sender: NSMenuItem) + func moveToEnd(_ sender: NSMenuItem) + @objc optional func manageBookmarks(_ sender: NSMenuItem) +} + +@objc protocol BookmarkMenuItemSelectors: BookmarksMenuItemSelectors { func openBookmarkInNewTab(_ sender: NSMenuItem) func openBookmarkInNewWindow(_ sender: NSMenuItem) @@ -30,11 +36,11 @@ import AppKit } -@objc protocol FolderMenuItemSelectors { +@objc protocol FolderMenuItemSelectors: BookmarksMenuItemSelectors { - func newFolder(_ sender: NSMenuItem) - func renameFolder(_ sender: NSMenuItem) + func editFolder(_ sender: NSMenuItem) func deleteFolder(_ sender: NSMenuItem) func openInNewTabs(_ sender: NSMenuItem) + func openAllInNewWindow(_ sender: NSMenuItem) } diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift deleted file mode 100644 index 31cc06dc04..0000000000 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// AddBookmarkFolderModalView.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AddBookmarkFolderModalView: ModalView { - - @State var model: AddBookmarkFolderModalViewModel = .init() - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(model.title) - .fontWeight(.semibold) - - HStack(spacing: 16) { - Text(UserText.newBookmarkDialogBookmarkNameTitle) - .frame(height: 22) - - TextField("", text: $model.folderName) - .accessibilityIdentifier("Title Text Field") - .textFieldStyle(.roundedBorder) - .disableAutocorrection(true) - } - .padding(.bottom, 4) - - HStack { - Spacer() - - Button(UserText.cancel) { - model.cancel(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.cancelAction) - - Button(model.addButtonTitle) { - model.addFolder(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.defaultAction) - .disabled(model.isAddButtonDisabled) - - } - } - .font(.system(size: 13)) - .padding() - .frame(width: 450, height: 131) - } - -} - -#Preview { - AddBookmarkFolderModalView() -} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index a0ab50c89a..f15465df36 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -23,59 +23,27 @@ struct AddBookmarkFolderPopoverView: ModalView { @ObservedObject var model: AddBookmarkFolderPopoverViewModel var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(UserText.newFolder) - .bold() - - VStack(alignment: .leading, spacing: 7) { - Text("Location:", comment: "Add Folder popover: parent folder picker title") - - BookmarkFolderPicker(folders: model.folders, selectedFolder: $model.parent) - .accessibilityIdentifier("bookmark.folder.folder.dropdown") - .disabled(model.isDisabled) - } - - VStack(alignment: .leading, spacing: 7) { - Text(UserText.newFolderDialogFolderNameTitle) - - TextField("", text: $model.folderName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.folder.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .disabled(model.isDisabled) - } - .padding(.bottom, 16) - - HStack { - Spacer() - - Button(action: { - model.cancel() - }) { - Text(UserText.cancel) - } - .accessibilityIdentifier("bookmark.add.cancel.button") - .disabled(model.isDisabled) - - Button(action: { - model.addFolder() - }) { - Text("Add Folder", comment: "Add Folder popover: Create folder button") - } - .keyboardShortcut(.defaultAction) - .accessibilityIdentifier("bookmark.add.add.folder.button") - .disabled(model.isAddFolderButtonDisabled || model.isDisabled) - } - } + AddEditBookmarkFolderView( + title: model.title, + buttonsState: .expanded, + folders: model.folders, + folderName: $model.folderName, + selectedFolder: $model.parent, + cancelActionTitle: model.cancelActionTitle, + isCancelActionDisabled: model.isCancelActionDisabled, + cancelAction: { _ in model.cancel() }, + defaultActionTitle: model.defaultActionTitle, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: { _ in model.addFolder() } + ) + .padding(.vertical, 16.0) .font(.system(size: 13)) - .padding() - .frame(width: 300, height: 229) - .background(Color(.popoverBackground)) + .frame(width: 320) } } #if DEBUG -#Preview { +#Preview("Add Folder - Light") { let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder 1", children: [ BookmarkFolder(id: "2", title: "Nested Folder with a name that in theory won‘t fit into the picker", children: [ @@ -94,5 +62,17 @@ struct AddBookmarkFolderPopoverView: ModalView { return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { print("CompletionHandler:", $0?.title ?? "") }) + .preferredColorScheme(.light) +} + +#Preview("Add Folder - Dark") { + let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bkman.loadBookmarks() + customAssertionFailure = { _, _, _ in } + + return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { + print("CompletionHandler:", $0?.title ?? "") + }) + .preferredColorScheme(.dark) } #endif diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift deleted file mode 100644 index 56941737fd..0000000000 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// AddBookmarkModalView.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit -import SwiftUI - -struct AddBookmarkModalView: ModalView { - - @State private(set) var model: AddBookmarkModalViewModel - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(model.title) - .fontWeight(.semibold) - - HStack(spacing: 16) { - VStack(alignment: .leading) { - Text("Title:", comment: "Add Bookmark dialog bookmark title field heading") - .frame(height: 22) - Text("Address:", comment: "Add Bookmark dialog bookmark url field heading") - .frame(height: 22) - } - - VStack { - TextField("", text: $model.bookmarkTitle) - .accessibilityIdentifier("Title Text Field") - .textFieldStyle(.roundedBorder) - TextField("", text: $model.bookmarkAddress) - .accessibilityIdentifier("URL Text Field") - .textFieldStyle(.roundedBorder) - .disableAutocorrection(true) - } - } - .padding(.bottom, 4) - - HStack { - Spacer() - - Button(UserText.cancel) { - model.cancel(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.cancelAction) - - Button(model.addButtonTitle) { - model.addOrSave(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.defaultAction) - .disabled(model.isAddButtonDisabled) - - } - } - .font(.system(size: 13)) - .padding() - .frame(width: 437, height: 164) - } - -} - -#Preview { - AddBookmarkModalView(model: AddBookmarkModalViewModel()) -} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index c8d9cadcbd..fba84b44bc 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -38,82 +38,32 @@ struct AddBookmarkPopoverView: View { @MainActor private var addBookmarkView: some View { - VStack(alignment: .leading, spacing: 19) { - Text("Bookmark Added", comment: "Bookmark Added popover title") - .fontWeight(.bold) - .padding(.bottom, 4) - - VStack(alignment: .leading, spacing: 10) { - TextField("", text: $model.bookmarkTitle) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - - HStack { - BookmarkFolderPicker(folders: model.folders, - selectedFolder: $model.selectedFolder) - .accessibilityIdentifier("bookmark.add.folder.dropdown") - - Button { - model.addFolderButtonAction() - } label: { - Image(.addFolder) - } - .accessibilityIdentifier("bookmark.add.new.folder.button") - .buttonStyle(StandardButtonStyle()) - } - } - - Divider() - - Button { - model.favoritesButtonAction() - } label: { - HStack(spacing: 8) { - if model.bookmark.isFavorite { - Image(.favoriteFilled) - Text(UserText.removeFromFavorites) - } else { - Image(.favorite) - Text(UserText.addToFavorites) - } - } - } - .accessibilityIdentifier("bookmark.add.add.to.favorites.button") - .buttonStyle(.borderless) - .foregroundColor(Color.button) - - HStack { - Spacer() - - Button { - model.removeButtonAction(dismiss: dismiss.callAsFunction) - } label: { - Text("Remove", comment: "Remove bookmark button title") - } - .accessibilityIdentifier("bookmark.add.remove.button") - - Button { - model.doneButtonAction(dismiss: dismiss.callAsFunction) - } label: { - Text(UserText.done) - } - .keyboardShortcut(.defaultAction) - .accessibilityIdentifier("bookmark.add.done.button") - } - - } + AddEditBookmarkView( + title: UserText.Bookmarks.Dialog.Title.addedBookmark, + buttonsState: .expanded, + bookmarkName: $model.bookmarkTitle, + bookmarkURLPath: nil, + isBookmarkFavorite: $model.isBookmarkFavorite, + folders: model.folders, + selectedFolder: $model.selectedFolder, + isURLFieldHidden: true, + addFolderAction: model.addFolderButtonAction, + otherActionTitle: UserText.remove, + isOtherActionDisabled: false, + otherAction: model.removeButtonAction, + defaultActionTitle: UserText.done, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: model.doneButtonAction + ) + .padding(.vertical, 16.0) .font(.system(size: 13)) - .padding(EdgeInsets(top: 19, leading: 19, bottom: 19, trailing: 19)) - .frame(width: 300, height: 229) - .background(Color(.popoverBackground)) + .frame(width: 320) } } #if DEBUG -#Preview { { +#Preview("Bookmark Added - Light") { let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [ @@ -133,5 +83,16 @@ struct AddBookmarkPopoverView: View { customAssertionFailure = { _, _, _ in } return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) -}() } + .preferredColorScheme(.light) +} + +#Preview("Bookmark Added - Dark") { + let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") + let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ + BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [])])) + bkman.loadBookmarks() + + return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) + .preferredColorScheme(.dark) +} #endif diff --git a/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift b/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift index c0aeab9bec..e7de655e8c 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift @@ -32,7 +32,7 @@ struct BookmarkFolderPicker: View { return popUpButton } content: { - PopupButtonItem(icon: .folder, title: UserText.bookmarks) + PopupButtonItem(icon: .bookmarksFolder, title: UserText.bookmarks) PopupButtonItem.separator() diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 541ad955b6..c405c23f61 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -63,6 +63,9 @@ final class BookmarkListViewController: NSViewController { contentMode: .bookmarksAndFolders, bookmarkManager: bookmarkManager, treeController: treeController, + onMenuRequestedAction: { [weak self] cell in + self?.showContextMenu(for: cell) + }, presentFaviconsFetcherOnboarding: { [weak self] in guard let self, let window = self.view.window else { return @@ -334,23 +337,18 @@ final class BookmarkListViewController: NSViewController { } @objc func newBookmarkButtonClicked(_ sender: AnyObject) { - delegate?.popover(shouldPreventClosure: true) - AddBookmarkModalView(model: AddBookmarkModalViewModel(currentTabWebsite: currentTabWebsite) { [weak delegate] _ in - delegate?.popover(shouldPreventClosure: false) - }).show(in: parent?.view.window) + let view = BookmarksDialogViewFactory.makeAddBookmarkView(currentTab: currentTabWebsite) + showDialog(view: view) } @objc func newFolderButtonClicked(_ sender: AnyObject) { - delegate?.popover(shouldPreventClosure: true) - AddBookmarkFolderModalView() - .show(in: parent?.view.window) { [weak delegate] in - delegate?.popover(shouldPreventClosure: false) - } + let parentFolder = sender.representedObject as? BookmarkFolder + let view = BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parentFolder) + showDialog(view: view) } @objc func openManagementInterface(_ sender: NSButton) { - WindowControllersManager.shared.showBookmarksTab() - delegate?.popoverShouldClose(self) + showManageBookmarks() } @objc func handleClick(_ sender: NSOutlineView) { @@ -425,6 +423,35 @@ final class BookmarkListViewController: NSViewController { outlineView.selectRowIndexes(indexes, byExtendingSelection: false) } + private func showContextMenu(for cell: BookmarkOutlineCellView) { + let row = outlineView.row(for: cell) + guard + let item = outlineView.item(atRow: row), + let contextMenu = ContextualMenu.menu(for: [item], target: self) + else { + return + } + + contextMenu.popUpAtMouseLocation(in: view) + } + +} + +private extension BookmarkListViewController { + + func showDialog(view: any ModalView) { + delegate?.popover(shouldPreventClosure: true) + + view.show(in: parent?.view.window) { [weak delegate] in + delegate?.popover(shouldPreventClosure: false) + } + } + + func showManageBookmarks() { + WindowControllersManager.shared.showBookmarksTab() + delegate?.popoverShouldClose(self) + } + } // MARK: - Menu Item Selectors @@ -439,11 +466,11 @@ extension BookmarkListViewController: NSMenuDelegate { } if outlineView.selectedRowIndexes.contains(row) { - return ContextualMenu.menu(for: outlineView.selectedItems, includeBookmarkEditMenu: false) + return ContextualMenu.menu(for: outlineView.selectedItems) } if let item = outlineView.item(atRow: row) { - return ContextualMenu.menu(for: [item], includeBookmarkEditMenu: false) + return ContextualMenu.menu(for: [item]) } else { return nil } @@ -498,7 +525,13 @@ extension BookmarkListViewController: BookmarkMenuItemSelectors { } func editBookmark(_ sender: NSMenuItem) { - // Unsupported in the list view for the initial release. + guard let bookmark = sender.representedObject as? Bookmark else { + assertionFailure("Failed to retrieve Bookmark from Edit Bookmark context menu item") + return + } + + let view = BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + showDialog(view: view) } func copyBookmark(_ sender: NSMenuItem) { @@ -527,6 +560,20 @@ extension BookmarkListViewController: BookmarkMenuItemSelectors { bookmarkManager.remove(objectsWithUUIDs: uuids) } + func manageBookmarks(_ sender: NSMenuItem) { + showManageBookmarks() + } + + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + } extension BookmarkListViewController: FolderMenuItemSelectors { @@ -535,18 +582,16 @@ extension BookmarkListViewController: FolderMenuItemSelectors { newFolderButtonClicked(sender) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") + func editFolder(_ sender: NSMenuItem) { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { + assertionFailure("Failed to retrieve Bookmark from Edit Folder context menu item") return } - delegate?.popover(shouldPreventClosure: true) - - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) - .show(in: parent?.view.window) { [weak delegate] in - delegate?.popover(shouldPreventClosure: false) - } + let view = BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) + showDialog(view: view) } func deleteFolder(_ sender: NSMenuItem) { @@ -560,15 +605,28 @@ extension BookmarkListViewController: FolderMenuItemSelectors { func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, - let children = (sender.representedObject as? BookmarkFolder)?.children else { - assertionFailure("Cannot open in new tabs") + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new tabs") return } - let tabs = children.compactMap { ($0 as? Bookmark)?.urlObject }.map { Tab(content: .url($0, source: .bookmark), shouldLoadInBackground: true, burnerMode: tabCollection.burnerMode) } + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) tabCollection.append(tabs: tabs) } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } // MARK: - BookmarkListPopover diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index a9b03ede97..ecc643b33d 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -32,12 +32,10 @@ private struct EditedBookmarkMetadata { final class BookmarkManagementDetailViewController: NSViewController, NSMenuItemValidation { - fileprivate enum Constants { - static let animationSpeed: TimeInterval = 0.3 - } - + private let toolbarButtonsStackView = NSStackView() private lazy var newBookmarkButton = MouseOverButton(title: " " + UserText.newBookmark, target: self, action: #selector(presentAddBookmarkModal)) private lazy var newFolderButton = MouseOverButton(title: " " + UserText.newFolder, target: self, action: #selector(presentAddFolderModal)) + private lazy var deleteItemsButton = MouseOverButton(title: " " + UserText.bookmarksBarContextMenuDelete, target: self, action: #selector(delete)) private lazy var separator = NSBox() private lazy var scrollView = NSScrollView() @@ -54,32 +52,10 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem private let bookmarkManager: BookmarkManager private var selectionState: BookmarkManagementSidebarViewController.SelectionState = .empty { didSet { - editingBookmarkIndex = nil reloadData() } } - private var isEditing: Bool { - return editingBookmarkIndex != nil - } - - private var editingBookmarkIndex: EditedBookmarkMetadata? { - didSet { - NSAnimationContext.runAnimationGroup { context in - context.allowsImplicitAnimation = true - context.duration = Constants.animationSpeed - - NSAppearance.withAppAppearance { - if editingBookmarkIndex != nil { - view.animator().layer?.backgroundColor = NSColor.backgroundSecondary.cgColor - } else { - view.animator().layer?.backgroundColor = NSColor.bookmarkPageBackground.cgColor - } - } - } - } - } - func update(selectionState: BookmarkManagementSidebarViewController.SelectionState) { self.selectionState = selectionState } @@ -101,34 +77,16 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem view.addSubview(separator) view.addSubview(scrollView) view.addSubview(emptyState) - view.addSubview(newBookmarkButton) - view.addSubview(newFolderButton) - - newBookmarkButton.bezelStyle = .shadowlessSquare - newBookmarkButton.cornerRadius = 4 - newBookmarkButton.normalTintColor = .button - newBookmarkButton.mouseDownColor = .buttonMouseDown - newBookmarkButton.mouseOverColor = .buttonMouseOver - newBookmarkButton.imageHugsTitle = true - newBookmarkButton.setContentHuggingPriority(.defaultHigh, for: .vertical) - newBookmarkButton.translatesAutoresizingMaskIntoConstraints = false - newBookmarkButton.alignment = .center - newBookmarkButton.font = .systemFont(ofSize: 13) - newBookmarkButton.image = .addBookmark - newBookmarkButton.imagePosition = .imageLeading - - newFolderButton.bezelStyle = .shadowlessSquare - newFolderButton.cornerRadius = 4 - newFolderButton.normalTintColor = .button - newFolderButton.mouseDownColor = .buttonMouseDown - newFolderButton.mouseOverColor = .buttonMouseOver - newFolderButton.imageHugsTitle = true - newFolderButton.setContentHuggingPriority(.defaultHigh, for: .vertical) - newFolderButton.translatesAutoresizingMaskIntoConstraints = false - newFolderButton.alignment = .center - newFolderButton.font = .systemFont(ofSize: 13) - newFolderButton.image = .addFolder - newFolderButton.imagePosition = .imageLeading + view.addSubview(toolbarButtonsStackView) + toolbarButtonsStackView.addArrangedSubview(newBookmarkButton) + toolbarButtonsStackView.addArrangedSubview(newFolderButton) + toolbarButtonsStackView.addArrangedSubview(deleteItemsButton) + toolbarButtonsStackView.translatesAutoresizingMaskIntoConstraints = false + toolbarButtonsStackView.distribution = .fill + + configureToolbar(button: newBookmarkButton, image: .addBookmark, isHidden: false) + configureToolbar(button: newFolderButton, image: .addFolder, isHidden: false) + configureToolbar(button: deleteItemsButton, image: .trash, isHidden: true) emptyState.addSubview(emptyStateImageView) emptyState.addSubview(emptyStateTitle) @@ -137,32 +95,27 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem emptyState.isHidden = true emptyState.translatesAutoresizingMaskIntoConstraints = false + importButton.translatesAutoresizingMaskIntoConstraints = false - emptyStateTitle.isEditable = false - emptyStateTitle.setContentHuggingPriority(.defaultHigh, for: .vertical) - emptyStateTitle.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - emptyStateTitle.translatesAutoresizingMaskIntoConstraints = false - emptyStateTitle.alignment = .center - emptyStateTitle.drawsBackground = false - emptyStateTitle.isBordered = false - emptyStateTitle.font = .systemFont(ofSize: 15, weight: .semibold) - emptyStateTitle.textColor = .labelColor - emptyStateTitle.attributedStringValue = NSAttributedString.make(UserText.bookmarksEmptyStateTitle, - lineHeight: 1.14, - kern: -0.23) - - emptyStateMessage.isEditable = false - emptyStateMessage.setContentHuggingPriority(.defaultHigh, for: .vertical) - emptyStateMessage.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - emptyStateMessage.translatesAutoresizingMaskIntoConstraints = false - emptyStateMessage.alignment = .center - emptyStateMessage.drawsBackground = false - emptyStateMessage.isBordered = false - emptyStateMessage.font = .systemFont(ofSize: 13) - emptyStateMessage.textColor = .labelColor - emptyStateMessage.attributedStringValue = NSAttributedString.make(UserText.bookmarksEmptyStateMessage, - lineHeight: 1.05, - kern: -0.08) + configureEmptyState( + label: emptyStateTitle, + font: .systemFont(ofSize: 15, weight: .semibold), + attributedTitle: .make( + UserText.bookmarksEmptyStateTitle, + lineHeight: 1.14, + kern: -0.23 + ) + ) + + configureEmptyState( + label: emptyStateMessage, + font: .systemFont(ofSize: 13), + attributedTitle: .make( + UserText.bookmarksEmptyStateMessage, + lineHeight: 1.05, + kern: -0.08 + ) + ) emptyStateImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) emptyStateImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) @@ -195,7 +148,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem tableView.selectionHighlightStyle = .none tableView.allowsMultipleSelection = true tableView.usesAutomaticRowHeights = true - tableView.action = #selector(handleClick) tableView.doubleAction = #selector(handleDoubleClick) tableView.delegate = self tableView.dataSource = self @@ -209,47 +161,47 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } private func setupLayout() { - newBookmarkButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48).isActive = true - view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 48).isActive = true - separator.topAnchor.constraint(equalTo: newBookmarkButton.bottomAnchor, constant: 24).isActive = true - emptyState.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 20).isActive = true - scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor).isActive = true - - view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - view.trailingAnchor.constraint(greaterThanOrEqualTo: newFolderButton.trailingAnchor, constant: 20).isActive = true - view.trailingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 58).isActive = true - newFolderButton.leadingAnchor.constraint(equalTo: newBookmarkButton.trailingAnchor, constant: 16).isActive = true - emptyState.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - newFolderButton.centerYAnchor.constraint(equalTo: newBookmarkButton.centerYAnchor).isActive = true - separator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 58).isActive = true - newBookmarkButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32).isActive = true - emptyState.topAnchor.constraint(greaterThanOrEqualTo: separator.bottomAnchor, constant: 8).isActive = true - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48).isActive = true - emptyState.centerXAnchor.constraint(equalTo: separator.centerXAnchor).isActive = true - - newBookmarkButton.heightAnchor.constraint(equalToConstant: 24).isActive = true - - newFolderButton.heightAnchor.constraint(equalToConstant: 24).isActive = true - - emptyStateMessage.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true + NSLayoutConstraint.activate([ + toolbarButtonsStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), + view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 48), + separator.topAnchor.constraint(equalTo: toolbarButtonsStackView.bottomAnchor, constant: 24), + emptyState.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 20), + scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor), - importButton.translatesAutoresizingMaskIntoConstraints = false - importButton.topAnchor.constraint(equalTo: emptyStateMessage.bottomAnchor, constant: 8).isActive = true - emptyState.heightAnchor.constraint(equalToConstant: 218).isActive = true - emptyStateMessage.topAnchor.constraint(equalTo: emptyStateTitle.bottomAnchor, constant: 8).isActive = true - importButton.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyStateImageView.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyState.widthAnchor.constraint(equalToConstant: 224).isActive = true - emptyStateImageView.topAnchor.constraint(equalTo: emptyState.topAnchor).isActive = true - emptyStateTitle.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyStateTitle.topAnchor.constraint(equalTo: emptyStateImageView.bottomAnchor, constant: 8).isActive = true + view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + view.trailingAnchor.constraint(greaterThanOrEqualTo: toolbarButtonsStackView.trailingAnchor, constant: 20), + view.trailingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 58), + emptyState.centerXAnchor.constraint(equalTo: view.centerXAnchor), + separator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 58), + toolbarButtonsStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 32), + emptyState.topAnchor.constraint(greaterThanOrEqualTo: separator.bottomAnchor, constant: 8), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), + emptyState.centerXAnchor.constraint(equalTo: separator.centerXAnchor), + + newBookmarkButton.heightAnchor.constraint(equalToConstant: 24), + newFolderButton.heightAnchor.constraint(equalToConstant: 24), + deleteItemsButton.heightAnchor.constraint(equalToConstant: 24), - emptyStateMessage.widthAnchor.constraint(equalToConstant: 192).isActive = true + emptyStateMessage.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), - emptyStateTitle.widthAnchor.constraint(equalToConstant: 192).isActive = true + importButton.topAnchor.constraint(equalTo: emptyStateMessage.bottomAnchor, constant: 8), + emptyState.heightAnchor.constraint(equalToConstant: 218), + emptyStateMessage.topAnchor.constraint(equalTo: emptyStateTitle.bottomAnchor, constant: 8), + importButton.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyStateImageView.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyState.widthAnchor.constraint(equalToConstant: 224), + emptyStateImageView.topAnchor.constraint(equalTo: emptyState.topAnchor), + emptyStateTitle.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyStateTitle.topAnchor.constraint(equalTo: emptyStateImageView.bottomAnchor, constant: 8), + + emptyStateMessage.widthAnchor.constraint(equalToConstant: 192), + + emptyStateTitle.widthAnchor.constraint(equalToConstant: 192), + + emptyStateImageView.widthAnchor.constraint(equalToConstant: 128), + emptyStateImageView.heightAnchor.constraint(equalToConstant: 96), + ]) - emptyStateImageView.widthAnchor.constraint(equalToConstant: 128).isActive = true - emptyStateImageView.heightAnchor.constraint(equalToConstant: 96).isActive = true } override func viewDidLoad() { @@ -264,15 +216,9 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem override func viewDidDisappear() { super.viewDidDisappear() - editingBookmarkIndex = nil reloadData() } - override func mouseUp(with event: NSEvent) { - // Clicking anywhere outside of the table view should end editing mode for a given cell. - updateEditingState(forRowAt: -1) - } - override func keyDown(with event: NSEvent) { if event.charactersIgnoringModifiers == String(UnicodeScalar(NSDeleteCharacter)!) { deleteSelectedItems() @@ -280,15 +226,13 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } fileprivate func reloadData() { - guard editingBookmarkIndex == nil else { - // If the table view is editing, the reload will be deferred until after the cell animation has completed. - return - } emptyState.isHidden = !(bookmarkManager.list?.topLevelEntities.isEmpty ?? true) let scrollPosition = tableView.visibleRect.origin tableView.reloadData() tableView.scroll(scrollPosition) + + updateToolbarButtons() } @objc func onImportClicked(_ sender: NSButton) { @@ -306,7 +250,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem let index = sender.clickedRow - guard index != -1, editingBookmarkIndex?.index != index, let entity = fetchEntity(at: index) else { + guard index != -1, let entity = fetchEntity(at: index) else { return } @@ -324,21 +268,13 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } } - @objc func handleClick(_ sender: NSTableView) { - let index = sender.clickedRow - - if index != editingBookmarkIndex?.index { - endEditing() - } - } - @objc func presentAddBookmarkModal(_ sender: Any) { - AddBookmarkModalView(model: AddBookmarkModalViewModel(parent: selectionState.folder)) + BookmarksDialogViewFactory.makeAddBookmarkView(parent: selectionState.folder) .show(in: view.window) } @objc func presentAddFolderModal(_ sender: Any) { - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(parent: selectionState.folder)) + BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: selectionState.folder) .show(in: view.window) } @@ -354,53 +290,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem return true } - private func endEditing() { - if let editingIndex = editingBookmarkIndex?.index { - self.editingBookmarkIndex = nil - animateEditingState(forRowAt: editingIndex, editing: false) - } - } - - private func updateEditingState(forRowAt index: Int) { - guard index != -1 else { - endEditing() - return - } - - if editingBookmarkIndex?.index == nil || editingBookmarkIndex?.index != index { - endEditing() - } - - if let entity = fetchEntity(at: index) { - editingBookmarkIndex = EditedBookmarkMetadata(uuid: entity.id, index: index) - animateEditingState(forRowAt: index, editing: true) - } else { - assertionFailure("\(#file): Failed to find entity when updating editing state") - } - } - - private func animateEditingState(forRowAt index: Int, editing: Bool, completion: (() -> Void)? = nil) { - if let cell = tableView.view(atColumn: 0, row: index, makeIfNecessary: false) as? BookmarkTableCellView, - let row = tableView.rowView(atRow: index, makeIfNecessary: false) as? BookmarkTableRowView { - - tableView.beginUpdates() - NSAnimationContext.runAnimationGroup { context in - context.allowsImplicitAnimation = true - context.duration = Constants.animationSpeed - context.completionHandler = completion - - cell.editing = editing - row.editing = editing - - row.layoutSubtreeIfNeeded() - cell.layoutSubtreeIfNeeded() - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(arrayLiteral: 0, index)) - } - - tableView.endUpdates() - } - } - private func totalRows() -> Int { switch selectionState { case .empty: @@ -444,12 +333,6 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi let rowView = BookmarkTableRowView() rowView.onSelectionChanged = onSelectionChanged - let entity = fetchEntity(at: row) - - if let uuid = editingBookmarkIndex?.uuid, uuid == entity?.id { - rowView.editing = true - } - return rowView } @@ -463,14 +346,12 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi if let bookmark = entity as? Bookmark { cell.update(from: bookmark) - cell.editing = bookmark.id == editingBookmarkIndex?.uuid if bookmark.favicon(.small) == nil { faviconsFetcherOnboarding?.presentOnboardingIfNeeded() } } else if let folder = entity as? BookmarkFolder { cell.update(from: folder) - cell.editing = folder.id == editingBookmarkIndex?.uuid } else { assertionFailure("Failed to cast bookmark") } @@ -573,6 +454,17 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } } + private func fetchEntityAndParent(at row: Int) -> (entity: BaseBookmarkEntity?, parentFolder: BookmarkFolder?) { + switch selectionState { + case .empty: + return (bookmarkManager.list?.topLevelEntities[safe: row], nil) + case .folder(let folder): + return (folder.children[safe: row], folder) + case .favorites: + return (bookmarkManager.list?.favoriteBookmarks[safe: row], nil) + } + } + private func index(for entity: Bookmark) -> Int? { switch selectionState { case .empty: @@ -610,11 +502,25 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } func onSelectionChanged() { - resetSelections() - let indexes = tableView.selectedRowIndexes - indexes.forEach { - let cell = self.tableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? BookmarkTableCellView - cell?.isSelected = true + func updateCellSelections() { + resetSelections() + tableView.selectedRowIndexes.forEach { + let cell = self.tableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? BookmarkTableCellView + cell?.isSelected = true + } + } + + updateCellSelections() + updateToolbarButtons() + } + + private func updateToolbarButtons() { + let shouldShowDeleteButton = tableView.selectedRowIndexes.count > 1 + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.25 + deleteItemsButton.animator().isHidden = !shouldShowDeleteButton + newBookmarkButton.animator().isHidden = shouldShowDeleteButton + newFolderButton.animator().isHidden = shouldShowDeleteButton } } @@ -633,13 +539,45 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } } +// MARK: - Private + +private extension BookmarkManagementDetailViewController { + + func configureToolbar(button: MouseOverButton, image: NSImage, isHidden: Bool) { + button.bezelStyle = .shadowlessSquare + button.cornerRadius = 4 + button.normalTintColor = .button + button.mouseDownColor = .buttonMouseDown + button.mouseOverColor = .buttonMouseOver + button.imageHugsTitle = true + button.setContentHuggingPriority(.defaultHigh, for: .vertical) + button.alignment = .center + button.font = .systemFont(ofSize: 13) + button.image = image + button.imagePosition = .imageLeading + button.isHidden = isHidden + } + + func configureEmptyState(label: NSTextField, font: NSFont, attributedTitle: NSAttributedString) { + label.isEditable = false + label.setContentHuggingPriority(.defaultHigh, for: .vertical) + label.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + label.translatesAutoresizingMaskIntoConstraints = false + label.alignment = .center + label.drawsBackground = false + label.isBordered = false + label.font = font + label.textColor = .labelColor + label.attributedStringValue = attributedTitle + } + +} + // MARK: - BookmarkTableCellViewDelegate extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate { func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) { - guard !isEditing else { return } - let row = tableView.row(for: cell) guard let bookmark = fetchEntity(at: row) as? Bookmark else { @@ -647,45 +585,8 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate return } - if let contextMenu = ContextualMenu.menu(for: [bookmark]), let cursorLocation = self.view.window?.mouseLocationOutsideOfEventStream { - let convertedLocation = self.view.convert(cursorLocation, from: nil) - contextMenu.items.forEach { item in - item.target = self - } - - contextMenu.popUp(positioning: nil, at: convertedLocation, in: self.view) - } - } - - func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { - let row = tableView.row(for: cell) - - guard let bookmark = fetchEntity(at: row) as? Bookmark else { - assertionFailure("BookmarkManagementDetailViewController: Tried to favorite object which is not bookmark") - return - } - - bookmark.isFavorite.toggle() - bookmarkManager.update(bookmark: bookmark) - } - - func bookmarkTableCellView(_ cell: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) { - let row = tableView.row(for: cell) - defer { - endEditing() - } - guard var bookmark = fetchEntity(at: row) as? Bookmark, bookmark.id == uuid else { - return - } - - if let url = newUrl.url, url.absoluteString != bookmark.url { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - let bookmarkTitle = (newTitle.isEmpty ? bookmark.title : newTitle).trimmingWhitespace() - if bookmark.title != bookmarkTitle { - bookmark.title = bookmarkTitle - bookmarkManager.update(bookmark: bookmark) - } + guard let contextMenu = ContextualMenu.menu(for: [bookmark], target: self) else { return } + contextMenu.popUpAtMouseLocation(in: view) } } @@ -695,20 +596,21 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate extension BookmarkManagementDetailViewController: NSMenuDelegate { func contextualMenuForClickedRows() -> NSMenu? { - guard !isEditing else { return nil } - let row = tableView.clickedRow guard row != -1 else { return ContextualMenu.menu(for: nil) } - if tableView.selectedRowIndexes.contains(row) { + // If only one item is selected try to get the item and its parent folder otherwise show the menu for multiple items. + if tableView.selectedRowIndexes.contains(row), tableView.selectedRowIndexes.count > 1 { return ContextualMenu.menu(for: self.selectedItems()) } - if let item = fetchEntity(at: row) { - return ContextualMenu.menu(for: [item]) + let (item, parent) = fetchEntityAndParent(at: row) + + if let item { + return ContextualMenu.menu(for: item, parentFolder: parent) } else { return nil } @@ -738,13 +640,15 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { presentAddFolderModal(sender) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { + func editFolder(_ sender: NSMenuItem) { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { assertionFailure("Failed to cast menu represented object to BookmarkFolder") return } - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) .show(in: view.window) } @@ -757,6 +661,16 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { bookmarkManager.remove(folder: folder) } + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + func openInNewTabs(_ sender: NSMenuItem) { if let children = (sender.representedObject as? BookmarkFolder)?.children { let bookmarks = children.compactMap { $0 as? Bookmark } @@ -768,6 +682,18 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { } } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { @@ -811,8 +737,10 @@ extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { } func editBookmark(_ sender: NSMenuItem) { - guard let bookmark = sender.representedObject as? Bookmark, let bookmarkIndex = index(for: bookmark) else { return } - updateEditingState(forRowAt: bookmarkIndex) + guard let bookmark = sender.representedObject as? Bookmark else { return } + + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + .show(in: view.window) } func copyBookmark(_ sender: NSMenuItem) { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index 6d0fdd6860..53e502383b 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -51,7 +51,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { private lazy var outlineView = BookmarksOutlineView(frame: scrollView.frame) private lazy var treeController = BookmarkTreeController(dataSource: treeControllerDataSource) - private lazy var dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) + private lazy var dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController, showMenuButtonOnHover: false) private var cancellables = Set() @@ -211,6 +211,13 @@ final class BookmarkManagementSidebarViewController: NSViewController { // MARK: NSOutlineView Configuration private func expandAndRestore(selectedNodes: [BookmarkNode]) { + // OutlineView doesn't allow multiple selections so there should be only one selected node at time. + let selectedNode = selectedNodes.first + // As the data source reloaded we need to refresh the previously selected nodes. + // Lets consider the scenario where we add a folder to a subfolder. + // When the folder is added we need to "refresh" the node because the previously selected node folder has changed (it has a child folder now). + var refreshedSelectedNodes: [BookmarkNode] = [] + treeController.visitNodes { node in if let objectID = (node.representedObject as? BaseBookmarkEntity)?.id { if dataSource.expandedNodesIDs.contains(objectID) { @@ -218,6 +225,11 @@ final class BookmarkManagementSidebarViewController: NSViewController { } else { outlineView.collapseItem(node) } + + // Add the node if it contains previously selected folder + if let folder = selectedNode?.representedObject as? BookmarkFolder, folder.id == objectID { + refreshedSelectedNodes.append(node) + } } // Expand the Bookmarks pseudo folder automatically. @@ -226,7 +238,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { } } - restoreSelection(to: selectedNodes) + restoreSelection(to: refreshedSelectedNodes) } private func restoreSelection(to nodes: [BookmarkNode]) { @@ -292,16 +304,20 @@ extension BookmarkManagementSidebarViewController: NSMenuDelegate { extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { func newFolder(_ sender: NSMenuItem) { - AddBookmarkFolderModalView().show(in: view.window) + let parent = sender.representedObject as? BookmarkFolder + BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parent) + .show(in: view.window) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") + func editFolder(_ sender: NSMenuItem) { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { + assertionFailure("Failed to cast menu represented object to BookmarkFolder") return } - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) .show(in: view.window) } @@ -314,17 +330,40 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { bookmarkManager.remove(folder: folder) } + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, - let children = (sender.representedObject as? BookmarkFolder)?.children else { - assertionFailure("Cannot open in new tabs") + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new tabs") return } - let tabs = children.compactMap { ($0 as? Bookmark)?.urlObject }.map { Tab(content: .url($0, source: .bookmark), shouldLoadInBackground: true, burnerMode: tabCollection.burnerMode) } + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) tabCollection.append(tabs: tabs) } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } #if DEBUG diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index 08e6c56953..603849bbbf 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -19,11 +19,24 @@ import AppKit import Foundation +protocol BookmarkOutlineCellViewDelegate: AnyObject { + func outlineCellViewRequestedMenu(_ cell: BookmarkOutlineCellView) +} + final class BookmarkOutlineCellView: NSTableCellView { private lazy var faviconImageView = NSImageView() private lazy var titleLabel = NSTextField(string: "Bookmark/Folder") private lazy var countLabel = NSTextField(string: "42") + private lazy var menuButton = NSButton(title: "", image: .settings, target: self, action: #selector(cellMenuButtonClicked)) + private lazy var favoriteImageView = NSImageView() + private lazy var trackingArea: NSTrackingArea = { + NSTrackingArea(rect: .zero, options: [.inVisibleRect, .activeAlways, .mouseEnteredAndExited], owner: self, userInfo: nil) + }() + + var shouldShowMenuButton = false + + weak var delegate: BookmarkOutlineCellViewDelegate? init(identifier: NSUserInterfaceItemIdentifier) { super.init(frame: .zero) @@ -34,10 +47,35 @@ final class BookmarkOutlineCellView: NSTableCellView { fatalError("\(type(of: self)): Bad initializer") } + override func updateTrackingAreas() { + super.updateTrackingAreas() + + guard !trackingAreas.contains(trackingArea), shouldShowMenuButton else { return } + addTrackingArea(trackingArea) + } + + override func mouseEntered(with event: NSEvent) { + guard shouldShowMenuButton else { return } + countLabel.isHidden = true + favoriteImageView.isHidden = true + menuButton.isHidden = false + } + + override func mouseExited(with event: NSEvent) { + guard shouldShowMenuButton else { return } + menuButton.isHidden = true + countLabel.isHidden = false + favoriteImageView.isHidden = false + } + + // MARK: - Private + private func setupUI() { addSubview(faviconImageView) addSubview(titleLabel) addSubview(countLabel) + addSubview(menuButton) + addSubview(favoriteImageView) faviconImageView.translatesAutoresizingMaskIntoConstraints = false faviconImageView.image = .bookmarkDefaultFavicon @@ -64,40 +102,74 @@ final class BookmarkOutlineCellView: NSTableCellView { countLabel.textColor = .blackWhite60 countLabel.lineBreakMode = .byClipping + menuButton.translatesAutoresizingMaskIntoConstraints = false + menuButton.contentTintColor = .button + menuButton.imagePosition = .imageTrailing + menuButton.isBordered = false + menuButton.isHidden = true + + favoriteImageView.translatesAutoresizingMaskIntoConstraints = false + favoriteImageView.imageScaling = .scaleProportionallyDown setupLayout() } private func setupLayout() { - faviconImageView.heightAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.widthAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5).isActive = true - faviconImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + NSLayoutConstraint.activate([ + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + faviconImageView.widthAnchor.constraint(equalToConstant: 16), + faviconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), + faviconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + + titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 10), + bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 6), + + countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5), + trailingAnchor.constraint(equalTo: countLabel.trailingAnchor), + + menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + menuButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 5), + menuButton.trailingAnchor.constraint(equalTo: trailingAnchor), + menuButton.topAnchor.constraint(equalTo: topAnchor), + menuButton.bottomAnchor.constraint(equalTo: bottomAnchor), + menuButton.widthAnchor.constraint(equalToConstant: 28), + + favoriteImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + favoriteImageView.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 5), + favoriteImageView.trailingAnchor.constraint(equalTo: menuButton.trailingAnchor), + favoriteImageView.heightAnchor.constraint(equalToConstant: 15), + favoriteImageView.widthAnchor.constraint(equalToConstant: 15), + ]) + faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .vertical) - titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 10).isActive = true - bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6).isActive = true - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 6).isActive = true titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) titleLabel.setContentHuggingPriority(.init(rawValue: 200), for: .horizontal) - countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5).isActive = true - trailingAnchor.constraint(equalTo: countLabel.trailingAnchor).isActive = true countLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) countLabel.setContentHuggingPriority(.required, for: .horizontal) } + @objc private func cellMenuButtonClicked() { + delegate?.outlineCellViewRequestedMenu(self) + } + + // MARK: - Public + func update(from bookmark: Bookmark) { faviconImageView.image = bookmark.favicon(.small) ?? .bookmarkDefaultFavicon titleLabel.stringValue = bookmark.title countLabel.stringValue = "" + favoriteImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil } func update(from folder: BookmarkFolder) { faviconImageView.image = .folder titleLabel.stringValue = folder.title + favoriteImageView.image = nil let totalChildBookmarks = folder.totalChildBookmarks if totalChildBookmarks > 0 { @@ -111,10 +183,13 @@ final class BookmarkOutlineCellView: NSTableCellView { faviconImageView.image = pseudoFolder.icon titleLabel.stringValue = pseudoFolder.name countLabel.stringValue = pseudoFolder.count > 0 ? String(pseudoFolder.count) : "" + favoriteImageView.image = nil } } +// MARK: - Preview + #if DEBUG @available(macOS 14.0, *) #Preview { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index 195ad48845..8bfdc3c4fb 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -22,8 +22,6 @@ import Foundation @objc protocol BookmarkTableCellViewDelegate: AnyObject { func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) - func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) - func bookmarkTableCellView(_ cellView: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) } @@ -33,53 +31,18 @@ final class BookmarkTableCellView: NSTableCellView { private lazy var titleLabel = NSTextField(string: "Bookmark") private lazy var bookmarkURLLabel = NSTextField(string: "URL") - private lazy var favoriteButton = NSButton(title: "", image: .favoriteFilledBorder, target: self, action: #selector(favoriteButtonClicked)) private lazy var accessoryImageView = NSImageView(image: .forward) - private var favoriteButtonBottomConstraint: NSLayoutConstraint! - private var favoriteButtonTrailingConstraint: NSLayoutConstraint! - private lazy var containerView = NSView() - private lazy var shadowView = NSBox() private lazy var menuButton = NSButton(title: "", image: .settings, target: self, action: #selector(cellMenuButtonClicked)) - // Shadow view constraints: - - private var shadowViewTopConstraint: NSLayoutConstraint! - private var shadowViewBottomConstraint: NSLayoutConstraint! - - // Container view constraints: - - private var titleLabelTopConstraint: NSLayoutConstraint! - private var titleLabelBottomConstraint: NSLayoutConstraint! - @objc func cellMenuButtonClicked(_ sender: NSButton) { delegate?.bookmarkTableCellViewRequestedMenu(sender, cell: self) } - @objc func favoriteButtonClicked(_ sender: NSButton) { - guard entity is Bookmark else { - assertionFailure("\(#file): Tried to favorite non-Bookmark object") - return - } - - delegate?.bookmarkTableCellViewToggledFavorite(cell: self) - } - weak var delegate: BookmarkTableCellViewDelegate? - var editing: Bool = false { - didSet { - if editing { - enterEditingMode() - } else { - exitEditingMode() - } - updateColors() - } - } - var isSelected = false { didSet { updateColors() @@ -96,16 +59,14 @@ final class BookmarkTableCellView: NSTableCellView { return } - accessoryImageView.isHidden = mouseInside || editing - menuButton.isHidden = !mouseInside || editing + accessoryImageView.isHidden = mouseInside + menuButton.isHidden = !mouseInside - if !mouseInside && !editing { + if !mouseInside { resetAppearanceFromBookmark() } - if !editing { - updateTitleLabelValue() - } + updateTitleLabelValue() } } @@ -130,36 +91,16 @@ final class BookmarkTableCellView: NSTableCellView { fatalError("\(type(of: self)): Bad initializer") } - // swiftlint:disable:next function_body_length private func setupUI() { autoresizingMask = [.width, .height] - addSubview(shadowView) addSubview(containerView) - shadowView.boxType = .custom - shadowView.borderColor = .clear - shadowView.borderWidth = 1 - shadowView.cornerRadius = 4 - shadowView.fillColor = .tableCellEditing - shadowView.translatesAutoresizingMaskIntoConstraints = false - shadowView.wantsLayer = true - shadowView.layer?.backgroundColor = NSColor.tableCellEditing.cgColor - shadowView.layer?.cornerRadius = 6 - - let shadow = NSShadow() - shadow.shadowOffset = NSSize(width: 0, height: -1) - shadow.shadowColor = NSColor.black.withAlphaComponent(0.2) - shadow.shadowBlurRadius = 2.0 - shadowView.shadow = shadow - containerView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(faviconImageView) containerView.addSubview(titleLabel) containerView.addSubview(menuButton) containerView.addSubview(accessoryImageView) - containerView.addSubview(bookmarkURLLabel) - containerView.addSubview(favoriteButton) faviconImageView.contentTintColor = .suggestionIcon faviconImageView.wantsLayer = true @@ -176,92 +117,50 @@ final class BookmarkTableCellView: NSTableCellView { titleLabel.font = .systemFont(ofSize: 13) titleLabel.textColor = .labelColor titleLabel.lineBreakMode = .byTruncatingTail - titleLabel.cell?.sendsActionOnEndEditing = true titleLabel.cell?.usesSingleLineMode = true titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) titleLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - titleLabel.delegate = self - - bookmarkURLLabel.focusRingType = .none - bookmarkURLLabel.isEditable = false - bookmarkURLLabel.isSelectable = false - bookmarkURLLabel.isBordered = false - bookmarkURLLabel.drawsBackground = false - bookmarkURLLabel.font = .systemFont(ofSize: 13) - bookmarkURLLabel.textColor = .secondaryLabelColor - bookmarkURLLabel.lineBreakMode = .byClipping - bookmarkURLLabel.translatesAutoresizingMaskIntoConstraints = false - bookmarkURLLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - bookmarkURLLabel.setContentHuggingPriority(.required, for: .vertical) - bookmarkURLLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - bookmarkURLLabel.delegate = self accessoryImageView.translatesAutoresizingMaskIntoConstraints = false - accessoryImageView.widthAnchor.constraint(equalToConstant: 22).isActive = true - accessoryImageView.heightAnchor.constraint(equalToConstant: 32).isActive = true menuButton.contentTintColor = .button menuButton.translatesAutoresizingMaskIntoConstraints = false menuButton.isBordered = false menuButton.isHidden = true - - favoriteButton.translatesAutoresizingMaskIntoConstraints = false - favoriteButton.isBordered = false } private func setupLayout() { + NSLayoutConstraint.activate([ + trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 3), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3), + bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 3), + containerView.topAnchor.constraint(equalTo: topAnchor, constant: 3), - trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: 3).isActive = true - shadowView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3).isActive = true - containerView.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor).isActive = true - containerView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true - containerView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true - containerView.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor).isActive = true + menuButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + faviconImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6), - bookmarkURLLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10).isActive = true - favoriteButtonTrailingConstraint = trailingAnchor.constraint(equalTo: favoriteButton.trailingAnchor, constant: 3) - favoriteButtonTrailingConstraint.isActive = true + accessoryImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 8), + trailingAnchor.constraint(equalTo: accessoryImageView.trailingAnchor, constant: 3), + faviconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + trailingAnchor.constraint(equalTo: menuButton.trailingAnchor, constant: 2), - menuButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8).isActive = true - faviconImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6).isActive = true - favoriteButton.topAnchor.constraint(equalTo: bookmarkURLLabel.bottomAnchor).isActive = true + menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), - accessoryImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 8).isActive = true - trailingAnchor.constraint(equalTo: accessoryImageView.trailingAnchor, constant: 3).isActive = true - faviconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - trailingAnchor.constraint(equalTo: menuButton.trailingAnchor, constant: 2).isActive = true - bookmarkURLLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor).isActive = true - trailingAnchor.constraint(equalTo: bookmarkURLLabel.trailingAnchor, constant: 16).isActive = true - menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true + menuButton.heightAnchor.constraint(equalToConstant: 32), + menuButton.widthAnchor.constraint(equalToConstant: 28), - favoriteButton.widthAnchor.constraint(equalToConstant: 24).isActive = true - favoriteButton.heightAnchor.constraint(equalToConstant: 24).isActive = true + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + faviconImageView.widthAnchor.constraint(equalToConstant: 16), - menuButton.heightAnchor.constraint(equalToConstant: 32).isActive = true - menuButton.widthAnchor.constraint(equalToConstant: 28).isActive = true + bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5), - faviconImageView.heightAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.widthAnchor.constraint(equalToConstant: 16).isActive = true - - shadowViewTopConstraint = shadowView.topAnchor.constraint(equalTo: topAnchor, constant: 3) - shadowViewTopConstraint.isActive = true - - shadowViewBottomConstraint = bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: 3) - shadowViewBottomConstraint.isActive = true - - titleLabelBottomConstraint = bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8) - titleLabelBottomConstraint.priority = .init(rawValue: 250) - titleLabelBottomConstraint.isActive = true - - favoriteButtonBottomConstraint = bottomAnchor.constraint(equalTo: favoriteButton.bottomAnchor, constant: 8) - favoriteButtonBottomConstraint.priority = .init(rawValue: 750) - favoriteButtonBottomConstraint.isActive = true - - titleLabelTopConstraint = titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5) - titleLabelTopConstraint.isActive = true + accessoryImageView.widthAnchor.constraint(equalToConstant: 22), + accessoryImageView.heightAnchor.constraint(equalToConstant: 32), + ]) } override var backgroundStyle: NSView.BackgroundStyle { @@ -314,84 +213,28 @@ final class BookmarkTableCellView: NSTableCellView { accessoryImageView.isHidden = false } - accessoryImageView.image = bookmark.isFavorite ? .favorite : nil - favoriteButton.image = bookmark.isFavorite ? .favoriteFilledBorder : .favorite + accessoryImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil titleLabel.stringValue = bookmark.title primaryTitleLabelValue = bookmark.title tertiaryTitleLabelValue = bookmark.url - bookmarkURLLabel.stringValue = bookmark.url } func update(from folder: BookmarkFolder) { self.entity = folder faviconImageView.image = .folder - accessoryImageView.image = .chevronNext16 + accessoryImageView.image = .chevronMediumRight16 primaryTitleLabelValue = folder.title tertiaryTitleLabelValue = nil } private func resetCellState() { self.entity = nil - editing = false mouseInside = false - bookmarkURLLabel.isHidden = true - favoriteButton.isHidden = true - titleLabelBottomConstraint.priority = .required - } - - private func enterEditingMode() { - titleLabel.isEditable = true - bookmarkURLLabel.isEditable = true - - shadowViewTopConstraint.constant = 10 - shadowViewBottomConstraint.constant = 10 - titleLabelTopConstraint.constant = 12 - favoriteButtonTrailingConstraint.constant = 11 - favoriteButtonBottomConstraint.constant = 18 - shadowView.isHidden = false - faviconImageView.isHidden = true - - bookmarkURLLabel.isHidden = false - favoriteButton.isHidden = false - titleLabelBottomConstraint.priority = .defaultLow - - hideTertiaryValueInTitleLabel() - - // Reluctantly use GCD as a workaround for a rare label layout issue, in which the text field shows no text upon becoming first responder. - DispatchQueue.main.async { - self.titleLabel.becomeFirstResponder() - } - } - - private func exitEditingMode() { - window?.makeFirstResponder(nil) - - titleLabel.isEditable = false - bookmarkURLLabel.isEditable = false - - titleLabelTopConstraint.constant = 5 - shadowViewTopConstraint.constant = 3 - shadowViewBottomConstraint.constant = 3 - favoriteButtonTrailingConstraint.constant = 3 - favoriteButtonBottomConstraint.constant = 8 - shadowView.isHidden = true - faviconImageView.isHidden = false - - bookmarkURLLabel.isHidden = true - favoriteButton.isHidden = true - titleLabelBottomConstraint.priority = .required - - if let editedBookmark = self.entity as? Bookmark { - delegate?.bookmarkTableCellView(self, - updatedBookmarkWithUUID: editedBookmark.id, - newTitle: titleLabel.stringValue, - newUrl: bookmarkURLLabel.stringValue) - } } private func updateColors() { - titleLabel.textColor = isSelected && !editing ? .white : .controlTextColor + titleLabel.textColor = isSelected ? .white : .controlTextColor menuButton.contentTintColor = isSelected ? .white : .button faviconImageView.contentTintColor = isSelected ? .white : .suggestionIcon accessoryImageView.contentTintColor = isSelected ? .white : .suggestionIcon @@ -428,11 +271,7 @@ final class BookmarkTableCellView: NSTableCellView { } private func updateTitleLabelValue() { - guard !editing else { - return - } - - if let tertiaryValue = tertiaryTitleLabelValue, mouseInside, !editing { + if let tertiaryValue = tertiaryTitleLabelValue, mouseInside { showTertiaryValueInTitleLabel(tertiaryValue) } else { hideTertiaryValueInTitleLabel() @@ -467,26 +306,6 @@ final class BookmarkTableCellView: NSTableCellView { } -extension BookmarkTableCellView: NSTextFieldDelegate { - - func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - switch commandSelector { - case #selector(cancelOperation) where self.editing: - self.resetAppearanceFromBookmark() - self.editing = false - return true - - case #selector(insertNewline) where self.editing: - self.editing = false - return true - - default: break - } - return false - } - -} - #if DEBUG @available(macOS 14.0, *) #Preview { @@ -517,19 +336,10 @@ extension BookmarkTableCellView { fatalError("init(coder:) has not been implemented") } - func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) { - cell.editing.toggle() - } + func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) {} func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { (cell.entity as? Bookmark)?.isFavorite.toggle() - cell.editing = false - } - - func bookmarkTableCellView(_ cellView: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) { - if cell.editing { - cell.editing = false - } } } } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift index ebd3a9f318..0f06f84788 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift @@ -16,14 +16,12 @@ // limitations under the License. // -import Foundation +import AppKit final class BookmarkTableRowView: NSTableRowView { var onSelectionChanged: (() -> Void)? - var editing = false - var hasPrevious = false { didSet { needsDisplay = true @@ -56,7 +54,7 @@ final class BookmarkTableRowView: NSTableRowView { backgroundColor.setFill() bounds.fill() - if mouseInside && !editing { + if mouseInside { let path = NSBezierPath(roundedRect: bounds, xRadius: 6, yRadius: 6) NSColor.rowHover.setFill() path.fill() @@ -68,8 +66,6 @@ final class BookmarkTableRowView: NSTableRowView { } override func drawSelection(in dirtyRect: NSRect) { - guard !editing else { return } - var roundedCorners = [NSBezierPath.Corners]() if !hasPrevious { diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift new file mode 100644 index 0000000000..2c0256bba4 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -0,0 +1,123 @@ +// +// AddEditBookmarkDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkDialogView: ModalView { + @ObservedObject private var viewModel: AddEditBookmarkDialogCoordinatorViewModel + + init(viewModel: AddEditBookmarkDialogCoordinatorViewModel) { + self.viewModel = viewModel + } + + var body: some View { + Group { + switch viewModel.viewState { + case .bookmark: + addEditBookmarkView + case .folder: + addFolderView + } + } + .font(.system(size: 13)) + } + + private var addEditBookmarkView: some View { + AddEditBookmarkView( + title: viewModel.bookmarkModel.title, + buttonsState: .compressed, + bookmarkName: $viewModel.bookmarkModel.bookmarkName, + bookmarkURLPath: $viewModel.bookmarkModel.bookmarkURLPath, + isBookmarkFavorite: $viewModel.bookmarkModel.isBookmarkFavorite, + folders: viewModel.bookmarkModel.folders, + selectedFolder: $viewModel.bookmarkModel.selectedFolder, + isURLFieldHidden: false, + addFolderAction: viewModel.addFolderAction, + otherActionTitle: viewModel.bookmarkModel.cancelActionTitle, + isOtherActionDisabled: viewModel.bookmarkModel.isOtherActionDisabled, + otherAction: viewModel.bookmarkModel.cancel, + defaultActionTitle: viewModel.bookmarkModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, + defaultAction: viewModel.bookmarkModel.addOrSave + ) + .frame(width: 448, height: 288) + } + + private var addFolderView: some View { + AddEditBookmarkFolderView( + title: viewModel.folderModel.title, + buttonsState: .compressed, + folders: viewModel.folderModel.folders, + folderName: $viewModel.folderModel.folderName, + selectedFolder: $viewModel.folderModel.selectedFolder, + cancelActionTitle: viewModel.folderModel.cancelActionTitle, + isCancelActionDisabled: viewModel.folderModel.isOtherActionDisabled, + cancelAction: { _ in + viewModel.dismissAction() + }, + defaultActionTitle: viewModel.folderModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.folderModel.isDefaultActionDisabled, + defaultAction: { _ in + viewModel.folderModel.addOrSave { + viewModel.dismissAction() + } + } + ) + .frame(width: 448, height: 210) + } +} + +// MARK: - Previews + +#if DEBUG +#Preview("Add Bookmark - Light Mode") { + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkView(parent: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Add Bookmark - Dark Mode") { + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkView(parent: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} + +#Preview("Edit Bookmark - Light Mode") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Edit Bookmark - Dark Mode") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift new file mode 100644 index 0000000000..f859311335 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -0,0 +1,107 @@ +// +// AddEditBookmarkFolderDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkFolderDialogView: ModalView { + @ObservedObject private var viewModel: AddEditBookmarkFolderDialogViewModel + + init(viewModel: AddEditBookmarkFolderDialogViewModel) { + self.viewModel = viewModel + } + + var body: some View { + AddEditBookmarkFolderView( + title: viewModel.title, + buttonsState: .compressed, + folders: viewModel.folders, + folderName: $viewModel.folderName, + selectedFolder: $viewModel.selectedFolder, + cancelActionTitle: viewModel.cancelActionTitle, + isCancelActionDisabled: viewModel.isOtherActionDisabled, + cancelAction: viewModel.cancel, + defaultActionTitle: viewModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.isDefaultActionDisabled, + defaultAction: viewModel.addOrSave + ) + .font(.system(size: 13)) + .frame(width: 448, height: 210) + } +} + +// MARK: - Previews +#if DEBUG +#Preview("Add Folder To Bookmarks - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Add Folder To Bookmarks Subfolder - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Edit Folder - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: bookmarkFolder, parentFolder: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Add Folder To Bookmarks - Dark") { + let store = BookmarkStoreMock(bookmarks: []) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} + +#Preview("Add Folder To Bookmarks Subfolder - Dark") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} + +#Preview("Edit Folder in Subfolder - Dark") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: bookmarkFolder, parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift new file mode 100644 index 0000000000..d89cfc7c93 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift @@ -0,0 +1,132 @@ +// +// AddEditBookmarkFolderView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkFolderView: View { + enum ButtonsState { + case compressed + case expanded + } + + let title: String + let buttonsState: ButtonsState + let folders: [FolderViewModel] + @Binding var folderName: String + @Binding var selectedFolder: BookmarkFolder? + + let cancelActionTitle: String + let isCancelActionDisabled: Bool + let cancelAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $folderName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkFolderPicker( + folders: folders, + selectedFolder: $selectedFolder + ) + .accessibilityIdentifier("bookmark.folder.folder.dropdown") + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(buttonsState), + otherButtonAction: .init( + title: cancelActionTitle, + keyboardShortCut: .cancelAction, + isDisabled: isCancelActionDisabled, + action: cancelAction + ), defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } +} + +private extension BookmarkDialogButtonsView.ViewState { + + init(_ state: AddEditBookmarkFolderView.ButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} + +#Preview("Compressed") { + @State var folderName = "" + @State var selectedFolder: BookmarkFolder? + + return AddEditBookmarkFolderView( + title: "Test Title", + buttonsState: .compressed, + folders: [], + folderName: $folderName, + selectedFolder: $selectedFolder, + cancelActionTitle: UserText.cancel, + isCancelActionDisabled: false, + cancelAction: { _ in }, + defaultActionTitle: UserText.save, + isDefaultActionDisabled: false, + defaultAction: { _ in } + ) +} + +#Preview("Expanded") { + @State var folderName = "" + @State var selectedFolder: BookmarkFolder? + + return AddEditBookmarkFolderView( + title: "Test Title", + buttonsState: .expanded, + folders: [], + folderName: $folderName, + selectedFolder: $selectedFolder, + cancelActionTitle: UserText.cancel, + isCancelActionDisabled: false, + cancelAction: { _ in }, + defaultActionTitle: UserText.save, + isDefaultActionDisabled: false, + defaultAction: { _ in } + ) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift new file mode 100644 index 0000000000..8d34889432 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift @@ -0,0 +1,113 @@ +// +// AddEditBookmarkView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkView: View { + let title: String + let buttonsState: BookmarksDialogButtonsState + + @Binding var bookmarkName: String + var bookmarkURLPath: Binding? + @Binding var isBookmarkFavorite: Bool + + let folders: [FolderViewModel] + @Binding var selectedFolder: BookmarkFolder? + + let isURLFieldHidden: Bool + + let addFolderAction: () -> Void + + let otherActionTitle: String + let isOtherActionDisabled: Bool + let otherAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $bookmarkName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.url, + content: TextField("", text: bookmarkURLPath ?? .constant("")) + .accessibilityIdentifier("bookmark.add.url.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)), + isContentViewHidden: isURLFieldHidden + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: folders, + selectedFolder: $selectedFolder, + onActionButton: addFolderAction + ) + ) + ) + BookmarkFavoriteView(isFavorite: $isBookmarkFavorite) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(buttonsState), + otherButtonAction: .init( + title: otherActionTitle, + isDisabled: isOtherActionDisabled, + action: otherAction + ), + defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } + +} + +// MARK: - BookmarksDialogButtonsState + +enum BookmarksDialogButtonsState { + case compressed + case expanded +} + +extension BookmarkDialogButtonsView.ViewState { + init(_ state: BookmarksDialogButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift new file mode 100644 index 0000000000..7726516704 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift @@ -0,0 +1,186 @@ +// +// BookmarkDialogButtonsView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct BookmarkDialogButtonsView: View { + private let viewState: ViewState + private let otherButtonAction: Action + private let defaultButtonAction: Action + @Environment(\.dismiss) private var dismiss + + init( + viewState: ViewState, + otherButtonAction: Action, + defaultButtonAction: Action + ) { + self.viewState = viewState + self.otherButtonAction = otherButtonAction + self.defaultButtonAction = defaultButtonAction + } + + var body: some View { + HStack { + if viewState == .compressed { + Spacer() + } + + actionButton(action: otherButtonAction, viewState: viewState) + + actionButton(action: defaultButtonAction, viewState: viewState) + } + } + + @MainActor + private func actionButton(action: Action, viewState: ViewState) -> some View { + Button { + action.action(dismiss.callAsFunction) + } label: { + Text(action.title) + .frame(height: viewState.height) + .frame(maxWidth: viewState.maxWidth) + } + .keyboardShortcut(action.keyboardShortCut) + .disabled(action.isDisabled) + .ifLet(action.accessibilityIdentifier) { view, value in + view.accessibilityIdentifier(value) + } + } +} + +// MARK: - BookmarkDialogButtonsView + Types + +extension BookmarkDialogButtonsView { + + enum ViewState: Equatable { + case compressed + case expanded + } + + struct Action { + let title: String + let keyboardShortCut: KeyboardShortcut? + let accessibilityIdentifier: String? + let isDisabled: Bool + let action: @MainActor (_ dismiss: () -> Void) -> Void + + init( + title: String, + accessibilityIdentifier: String? = nil, + keyboardShortCut: KeyboardShortcut? = nil, + isDisabled: Bool = false, + action: @MainActor @escaping (_ dismiss: () -> Void) -> Void + ) { + self.title = title + self.keyboardShortCut = keyboardShortCut + self.accessibilityIdentifier = accessibilityIdentifier + self.isDisabled = isDisabled + self.action = action + } + } +} + +// MARK: - BookmarkDialogButtonsView.ViewState + +private extension BookmarkDialogButtonsView.ViewState { + + var maxWidth: CGFloat? { + switch self { + case .compressed: + return nil + case .expanded: + return .infinity + } + } + + var height: CGFloat? { + switch self { + case .compressed: + return nil + case .expanded: + return 28.0 + } + } + +} + +// MARK: - Preview + +#Preview("Compressed - Disable Default Button") { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: true, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} + +#Preview("Compressed - Enabled Default Button") { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: false, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} + +#Preview("Expanded - Disable Default Button") { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: true, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} + +#Preview("Expanded - Enable Default Button") { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: false, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift new file mode 100644 index 0000000000..ea49712abb --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift @@ -0,0 +1,50 @@ +// +// BookmarkDialogContainerView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogContainerView: View { + private let title: String + @ViewBuilder private let middleSection: () -> Content + @ViewBuilder private let bottomSection: () -> Buttons + + init( + title: String, + @ViewBuilder middleSection: @escaping () -> Content, + @ViewBuilder bottomSection: @escaping () -> Buttons + ) { + self.title = title + self.middleSection = middleSection + self.bottomSection = bottomSection + } + + var body: some View { + TieredDialogView( + verticalSpacing: 16.0, + horizontalPadding: 20.0, + top: { + Text(title) + .foregroundColor(.primary) + .fontWeight(.semibold) + }, + center: middleSection, + bottom: bottomSection + ) + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift new file mode 100644 index 0000000000..8081abc14d --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift @@ -0,0 +1,76 @@ +// +// BookmarkDialogFolderManagementView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogFolderManagementView: View { + private let folders: [FolderViewModel] + private var selectedFolder: Binding + private let onActionButton: @MainActor () -> Void + + init( + folders: [FolderViewModel], + selectedFolder: Binding, + onActionButton: @escaping @MainActor () -> Void + ) { + self.folders = folders + self.selectedFolder = selectedFolder + self.onActionButton = onActionButton + } + + var body: some View { + HStack { + BookmarkFolderPicker( + folders: folders, + selectedFolder: selectedFolder + ) + .accessibilityIdentifier("bookmark.add.folder.dropdown") + + Button { + onActionButton() + } label: { + Image(.addFolder) + } + .accessibilityIdentifier("bookmark.add.new.folder.button") + .buttonStyle(StandardButtonStyle()) + } + } +} + +#Preview { + @State var selectedFolder: BookmarkFolder? = BookmarkFolder(id: "1", title: "Nested Folder", children: []) + let folderViewModels: [FolderViewModel] = [ + .init( + entity: .init( + id: "1", + title: "Nested Folder", + parentFolderUUID: nil, + children: [] + ), + level: 1 + ) + ] + + return BookmarkDialogFolderManagementView( + folders: folderViewModels, + selectedFolder: $selectedFolder, + onActionButton: {} + ) + .frame(width: 400) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift new file mode 100644 index 0000000000..864b0cdb13 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift @@ -0,0 +1,111 @@ +// +// BookmarkDialogStackedContentView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogStackedContentView: View { + private let items: [Item] + + init(_ items: Item...) { + self.items = items + } + + init(_ items: [Item]) { + self.items = items + } + + var body: some View { + TwoColumnsListView( + horizontalSpacing: 16.0, + verticalSpacing: 20.0, + rowHeight: 22.0, + leftColumn: { + ForEach(items, id: \.title) { item in + if !item.isContentViewHidden { + Text(item.title) + .foregroundColor(.primary) + .fontWeight(.medium) + } + } + }, + rightColumn: { + ForEach(items, id: \.title) { item in + if !item.isContentViewHidden { + item.content + } + } + } + ) + } +} + +// MARK: - BookmarkModalStackedContentView + Item + +extension BookmarkDialogStackedContentView { + struct Item { + fileprivate let title: String + fileprivate let content: AnyView + fileprivate let isContentViewHidden: Bool + + init(title: String, content: any View, isContentViewHidden: Bool = false) { + self.title = title + self.content = AnyView(content) + self.isContentViewHidden = isContentViewHidden + } + } +} + +// MARK: - Preview + +#Preview { + @State var name: String = "DuckDuckGo" + @State var url: String = "https://www.duckduckgo.com" + @State var selectedFolder: BookmarkFolder? + + return BookmarkDialogStackedContentView( + .init( + title: "Name", + content: + TextField("", text: $name) + .textFieldStyle(.roundedBorder) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + + ), + .init( + title: "URL", + content: + TextField("", text: $url) + .textFieldStyle(.roundedBorder) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: "Location", + content: + BookmarkDialogFolderManagementView( + folders: [], + selectedFolder: $selectedFolder, + onActionButton: { } + ) + ) + ) + .padding([.horizontal, .vertical]) + .frame(width: 400) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift new file mode 100644 index 0000000000..9778ab0d5c --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift @@ -0,0 +1,47 @@ +// +// BookmarkFavoriteView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions +import PreferencesViews + +struct BookmarkFavoriteView: View { + @Binding var isFavorite: Bool + + var body: some View { + Toggle(isOn: $isFavorite) { + HStack(spacing: 6) { + Image(.favoriteFilledBorder) + Text(UserText.addToFavorites) + .foregroundColor(.primary) + } + } + .toggleStyle(.checkbox) + .accessibilityIdentifier("bookmark.add.add.to.favorites.button") + } +} + +#Preview("Favorite") { + BookmarkFavoriteView(isFavorite: .constant(true)) + .frame(width: 300) +} + +#Preview("Not Favorite") { + BookmarkFavoriteView(isFavorite: .constant(false)) + .frame(width: 300) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift new file mode 100644 index 0000000000..b29b50bbbb --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -0,0 +1,93 @@ +// +// BookmarksDialogViewFactory.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@MainActor +enum BookmarksDialogViewFactory { + + /// Creates an instance of AddEditBookmarkFolderDialogView for adding a Bookmark Folder. + /// - Parameters: + /// - parentFolder: An optional `BookmarkFolder`. When adding a folder to the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the new folder should be within. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkFolderDialogView. + static func makeAddBookmarkFolderView(parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: parentFolder), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + } + + /// Creates an instance of AddEditBookmarkFolderDialogView for editing a Bookmark Folder. + /// - Parameters: + /// - folder: The `BookmarkFolder` to edit. + /// - parentFolder: An optional `BookmarkFolder`. When editing a folder within the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the folder belongs to. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkFolderDialogView. + static func makeEditBookmarkFolderView(folder: BookmarkFolder, parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: parentFolder), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark with the specified web page. + /// - Parameters: + /// - currentTab: An optional `WebsiteInfo`. When adding a bookmark from the bookmark shortcut panel, if the `Tab` has loaded a web page pass the information via the `currentTab`. If the `Tab` has not loaded a tab pass `nil`. If adding a `Bookmark` from the `Manage Bookmark` settings page, pass `nil`. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView. + static func makeAddBookmarkView(currentTab: WebsiteInfo?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: currentTab), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark with the specified parent folder. + /// - Parameters: + /// - parentFolder: An optional `BookmarkFolder`. When adding a bookmark from the bookmark management view, if the user select a parent folder pass this value won't be `nil`. Otherwise, if no folder is selected this value will be `nil`. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView. + static func makeAddBookmarkView(parent: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: parent), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark from the Favorites view in the empty Tab. + /// - Parameter bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView, + static func makeAddFavoriteView(bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: true), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for editing a Bookmark. + /// - Parameters: + /// - bookmark: The `Bookmark` to edit. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView. + static func makeEditBookmarkView(bookmark: Bookmark, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + +} + +private extension BookmarksDialogViewFactory { + + private static func makeAddEditBookmarkDialogView(viewModel: AddEditBookmarkDialogViewModel, bookmarkManager: BookmarkManager) -> AddEditBookmarkDialogView { + let addFolderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: viewModel, folderModel: addFolderViewModel) + return AddEditBookmarkDialogView(viewModel: viewModel) + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift deleted file mode 100644 index ac2701e3ca..0000000000 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// AddBookmarkFolderModalViewModel.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct AddBookmarkFolderModalViewModel { - - let bookmarkManager: BookmarkManager - - let title: String - let addButtonTitle: String - let originalFolder: BookmarkFolder? - let parent: BookmarkFolder? - - var folderName: String = "" - - var isAddButtonDisabled: Bool { - folderName.trimmingWhitespace().isEmpty - } - - init(folder: BookmarkFolder, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - completionHandler: @escaping (BookmarkFolder?) -> Void = { _ in }) { - self.bookmarkManager = bookmarkManager - self.folderName = folder.title - self.originalFolder = folder - self.parent = nil - self.title = UserText.renameFolder - self.addButtonTitle = UserText.save - } - - init(parent: BookmarkFolder? = nil, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - completionHandler: @escaping (BookmarkFolder?) -> Void = { _ in }) { - self.bookmarkManager = bookmarkManager - self.originalFolder = nil - self.parent = parent - self.title = UserText.newFolder - self.addButtonTitle = UserText.newFolderDialogAdd - } - - func cancel(dismiss: () -> Void) { - dismiss() - } - - func addFolder(dismiss: () -> Void) { - guard !folderName.isEmpty else { - assertionFailure("folderName is empty, button should be disabled") - return - } - - let folderName = folderName.trimmingWhitespace() - if let folder = originalFolder { - folder.title = folderName - bookmarkManager.update(folder: folder) - - } else { - bookmarkManager.makeFolder(for: folderName, parent: parent, completion: { _ in }) - } - - dismiss() - } - -} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift index c16625a362..a1359fa61b 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift @@ -22,15 +22,30 @@ import Foundation final class AddBookmarkFolderPopoverViewModel: ObservableObject { private let bookmarkManager: BookmarkManager - let folders: [FolderViewModel] - @Published var parent: BookmarkFolder? + private let completionHandler: (BookmarkFolder?) -> Void + @Published var parent: BookmarkFolder? @Published var folderName: String = "" - @Published private(set) var isDisabled = false - private let completionHandler: (BookmarkFolder?) -> Void + var title: String { + UserText.Bookmarks.Dialog.Title.addFolder + } + + var cancelActionTitle: String { + UserText.cancel + } + + var defaultActionTitle: String { + UserText.Bookmarks.Dialog.Action.addFolder + } + + let isCancelActionDisabled = false + + var isDefaultActionButtonDisabled: Bool { + folderName.trimmingWhitespace().isEmpty || isDisabled + } init(bookmark: Bookmark? = nil, folderName: String = "", diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift deleted file mode 100644 index 15554afcbc..0000000000 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// AddBookmarkModalViewModel.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct AddBookmarkModalViewModel { - - let bookmarkManager: BookmarkManager - - let title: String - let addButtonTitle: String - var isFavorite: Bool - - private let originalBookmark: Bookmark? - private let parent: BookmarkFolder? - - private let completionHandler: (Bookmark?) -> Void - - var bookmarkTitle: String = "" - var bookmarkAddress: String = "" - - private var hasValidInput: Bool { - guard let url = bookmarkAddress.url else { return false } - - return !bookmarkTitle.trimmingWhitespace().isEmpty && url.isValid - } - - var isAddButtonDisabled: Bool { !hasValidInput } - - func cancel(dismiss: () -> Void) { - completionHandler(nil) - dismiss() - } - - func addOrSave(dismiss: () -> Void) { - guard let url = bookmarkAddress.url else { - assertionFailure("invalid URL, button should be disabled") - return - } - - var result: Bookmark? - let bookmarkTitle = bookmarkTitle.trimmingWhitespace() - if var bookmark = originalBookmark ?? bookmarkManager.getBookmark(for: url) { - - if url.absoluteString != bookmark.url { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - if bookmark.title != bookmarkTitle || bookmark.isFavorite != isFavorite { - bookmark.title = bookmarkTitle - bookmark.isFavorite = isFavorite - bookmarkManager.update(bookmark: bookmark) - } - - result = bookmark - - } else if !bookmarkManager.isUrlBookmarked(url: url) { - result = bookmarkManager.makeBookmark(for: url, title: bookmarkTitle, isFavorite: isFavorite, index: nil, parent: parent) - } - - completionHandler(result) - dismiss() - } - - init(isFavorite: Bool = false, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - currentTabWebsite website: WebsiteInfo? = nil, - parent: BookmarkFolder? = nil, - completionHandler: @escaping (Bookmark?) -> Void = { _ in }) { - - self.bookmarkManager = bookmarkManager - - self.isFavorite = isFavorite - self.title = isFavorite ? UserText.addFavorite : UserText.newBookmark - self.addButtonTitle = UserText.bookmarkDialogAdd - - if let website, - !LocalBookmarkManager.shared.isUrlBookmarked(url: website.url) { - bookmarkTitle = website.title ?? "" - bookmarkAddress = website.url.absoluteString - } - self.parent = parent - self.originalBookmark = nil - - self.completionHandler = completionHandler - } - - init(bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - originalBookmark: Bookmark?, - isFavorite: Bool = false, - completionHandler: @escaping (Bookmark?) -> Void = { _ in }) { - - self.bookmarkManager = bookmarkManager - - self.isFavorite = isFavorite - if originalBookmark != nil { - self.title = isFavorite ? UserText.editFavorite : UserText.updateBookmark - self.addButtonTitle = UserText.save - } else { - self.title = isFavorite ? UserText.addFavorite : UserText.newBookmark - self.addButtonTitle = UserText.bookmarkDialogAdd - } - - self.parent = nil - self.originalBookmark = originalBookmark - - bookmarkTitle = originalBookmark?.title ?? "" - bookmarkAddress = originalBookmark?.url ?? "" - - self.completionHandler = completionHandler - } - -} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift index b474e148b3..88168901ed 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift @@ -38,8 +38,24 @@ final class AddBookmarkPopoverViewModel: ObservableObject { } } + @Published var isBookmarkFavorite: Bool { + didSet { + bookmark.isFavorite = isBookmarkFavorite + bookmarkManager.update(bookmark: bookmark) + } + } + + @Published var bookmarkTitle: String { + didSet { + bookmark.title = bookmarkTitle.trimmingWhitespace() + bookmarkManager.update(bookmark: bookmark) + } + } + @Published var addFolderViewModel: AddBookmarkFolderPopoverViewModel? + let isDefaultActionButtonDisabled: Bool = false + private var bookmarkListCancellable: AnyCancellable? init(bookmark: Bookmark, @@ -47,6 +63,7 @@ final class AddBookmarkPopoverViewModel: ObservableObject { self.bookmarkManager = bookmarkManager self.bookmark = bookmark self.bookmarkTitle = bookmark.title + self.isBookmarkFavorite = bookmark.isFavorite bookmarkListCancellable = bookmarkManager.listPublisher .receive(on: DispatchQueue.main) @@ -74,12 +91,6 @@ final class AddBookmarkPopoverViewModel: ObservableObject { dismiss() } - func favoritesButtonAction() { - bookmark.isFavorite.toggle() - - bookmarkManager.update(bookmark: bookmark) - } - func addFolderButtonAction() { addFolderViewModel = .init(bookmark: bookmark, bookmarkManager: bookmarkManager) { [bookmark, bookmarkManager, weak self] newFolder in if let newFolder { @@ -98,14 +109,6 @@ final class AddBookmarkPopoverViewModel: ObservableObject { addFolderViewModel = nil } - @Published var bookmarkTitle: String { - didSet { - bookmark.title = bookmarkTitle.trimmingWhitespace() - - bookmarkManager.update(bookmark: bookmark) - } - } - } struct FolderViewModel: Identifiable, Equatable { diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift new file mode 100644 index 0000000000..27fc0c64e5 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift @@ -0,0 +1,74 @@ +// +// AddEditBookmarkDialogCoordinatorViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +final class AddEditBookmarkDialogCoordinatorViewModel: ObservableObject { + @ObservedObject var bookmarkModel: BookmarkViewModel + @ObservedObject var folderModel: AddFolderViewModel + @Published var viewState: ViewState + + private var cancellables: Set = [] + + init(bookmarkModel: BookmarkViewModel, folderModel: AddFolderViewModel) { + self.bookmarkModel = bookmarkModel + self.folderModel = folderModel + viewState = .bookmark + bind() + } + + func dismissAction() { + viewState = .bookmark + } + + func addFolderAction() { + folderModel.selectedFolder = bookmarkModel.selectedFolder + viewState = .folder + } + + private func bind() { + bookmarkModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.addFolderPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] bookmarkFolder in + self?.bookmarkModel.selectedFolder = bookmarkFolder + } + .store(in: &cancellables) + } +} + +extension AddEditBookmarkDialogCoordinatorViewModel { + enum ViewState { + case bookmark + case folder + } +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift new file mode 100644 index 0000000000..0aa03eade1 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -0,0 +1,216 @@ +// +// AddEditBookmarkDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +protocol BookmarkDialogEditing: BookmarksDialogViewModel { + var bookmarkName: String { get set } + var bookmarkURLPath: String { get set } + var isBookmarkFavorite: Bool { get set } + + var isURLFieldHidden: Bool { get } +} + +@MainActor +final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { + + /// The type of operation to perform on a bookmark. + enum Mode { + /// Add a new bookmark. Bookmarks can have a parent folder but not necessarily. + /// If the users add a bookmark to the root `Bookmarks` folder, then the parent folder is `nil`. + /// If the users add a bookmark to a different folder then the parent folder is not `nil`. + /// If the users add a bookmark from the bookmark shortcut and `Tab` has a page loaded, then the `tabWebsite` is not `nil`. + /// When adding a bookmark from favorite screen the `shouldPresetFavorite` flag should be set to `true`. + case add(tabWebsite: WebsiteInfo? = nil, parentFolder: BookmarkFolder? = nil, shouldPresetFavorite: Bool = false) + /// Edit an existing bookmark. + case edit(bookmark: Bookmark) + } + + @Published var bookmarkName: String + @Published var bookmarkURLPath: String + @Published var isBookmarkFavorite: Bool + + @Published private(set) var folders: [FolderViewModel] + @Published var selectedFolder: BookmarkFolder? + + private var folderCancellable: AnyCancellable? + + var title: String { + mode.title + } + + let isURLFieldHidden: Bool = false + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + private var hasValidInput: Bool { + guard let url = bookmarkURLPath.url else { return false } + return !bookmarkName.trimmingWhitespace().isEmpty && url.isValid + } + + let isOtherActionDisabled: Bool = false + + var isDefaultActionDisabled: Bool { !hasValidInput } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + + init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { + let isFavorite = mode.bookmarkURL.flatMap(bookmarkManager.isUrlFavorited) ?? false + self.mode = mode + self.bookmarkManager = bookmarkManager + folders = .init(bookmarkManager.list) + switch mode { + case let .add(websiteInfo, parentFolder, shouldPresetFavorite): + // When adding a new bookmark with website info we need to show the bookmark name and URL only if the bookmark is not bookmarked already. + // Scenario we click on the "Add Bookmark" button from Bookmarks shortcut Panel. If Tab has a Bookmark loaded we present the dialog with prepopulated name and URL from the tab. + // If we save and click again on the "Add Bookmark" button we don't want to try re-add the same bookmark. Hence we present a dialog that is not pre-populated. + let isAlreadyBookmarked = websiteInfo.flatMap { bookmarkManager.isUrlBookmarked(url: $0.url) } ?? false + let websiteName = isAlreadyBookmarked ? "" : websiteInfo?.title ?? "" + let websiteURLPath = isAlreadyBookmarked ? "" : websiteInfo?.url.absoluteString ?? "" + bookmarkName = websiteName + bookmarkURLPath = websiteURLPath + isBookmarkFavorite = shouldPresetFavorite ? true : isFavorite + selectedFolder = parentFolder + case let .edit(bookmark): + bookmarkName = bookmark.title + bookmarkURLPath = bookmark.urlObject?.absoluteString ?? "" + isBookmarkFavorite = isFavorite + selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity + } + + bind() + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + guard let url = bookmarkURLPath.url else { + assertionFailure("Invalid URL, default action button should be disabled.") + return + } + + let trimmedBookmarkName = bookmarkName.trimmingWhitespace() + + switch mode { + case .add: + addBookmark(withURL: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, to: selectedFolder) + case let .edit(bookmark): + updateBookmark(bookmark, url: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, location: selectedFolder) + } + dismiss() + } +} + +private extension AddEditBookmarkDialogViewModel { + + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) + }) + } + + func updateBookmark(_ bookmark: Bookmark, url: URL, name: String, isFavorite: Bool, location: BookmarkFolder?) { + // If the URL or Title or Favorite is changed update bookmark + if bookmark.url != url.absoluteString || bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { + bookmarkManager.update(bookmark: bookmark, withURL: url, title: name, isFavorite: isFavorite) + } + + // If the bookmark changed parent location, move it. + if shouldMove(bookmark: bookmark) { + let parentFolder: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFolder, completion: { _ in }) + } + } + + func addBookmark(withURL url: URL, name: String, isFavorite: Bool, to parent: BookmarkFolder?) { + // If a bookmark already exist with the new URL, update it + if let existingBookmark = bookmarkManager.getBookmark(for: url) { + updateBookmark(existingBookmark, url: url, name: name, isFavorite: isFavorite, location: parent) + } else { + bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) + } + } + + func shouldMove(bookmark: Bookmark) -> Bool { + // There's a discrepancy in representing the root folder. A bookmark belonging to the root folder has `parentFolderUUID` equal to `bookmarks_root`. + // There's no `BookmarkFolder` to represent the root folder, so the root folder is represented by a nil selectedFolder. + // Move Bookmarks if its parent folder is != from the selected folder but ONLY if: + // - The selected folder is not nil. This ensure we're comparing a subfolder with any bookmark parent folder. + // - The selected folder is nil and the bookmark parent folder is not the root folder. This ensure we're not unnecessarily moving the items within the same root folder. + bookmark.parentFolderUUID != selectedFolder?.id && (selectedFolder != nil || selectedFolder == nil && !bookmark.isParentFolderRoot) + } +} + +private extension AddEditBookmarkDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addBookmark + case .edit: + return UserText.Bookmarks.Dialog.Title.editBookmark + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addBookmark + case .edit: + return UserText.save + } + } + + var bookmarkURL: URL? { + switch self { + case let .add(tabInfo, _, _): + return tabInfo?.url + case let .edit(bookmark): + return bookmark.urlObject + } + } + +} + +private extension Bookmark { + + var isParentFolderRoot: Bool { + parentFolderUUID == "bookmarks_root" + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift new file mode 100644 index 0000000000..62c1e0356c --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -0,0 +1,181 @@ +// +// AddEditBookmarkFolderDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +protocol BookmarkFolderDialogEditing: BookmarksDialogViewModel { + var addFolderPublisher: AnyPublisher { get } + var folderName: String { get set } +} + +@MainActor +final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { + + /// The type of operation to perform on a folder + enum Mode { + /// Add a new folder. Folders can have a parent folder but not necessarily. + /// If the users add a folder to a folder whose parent is the root `Bookmarks` folder, then the parent folder is `nil`. + /// If the users add a folder to a folder whose parent is not the root `Bookmarks` folder, then the parent folder is not `nil`. + case add(parentFolder: BookmarkFolder? = nil) + /// Edit an existing folder. Existing folder can have a parent folder but not necessarily. + /// If the users edit a folder whose parent is the root `Bookmarks` folder, then the parent folder is `nil` + /// If the users edit a folder whose parent is not the root `Bookmarks` folder, then the parent folder is not `nil`. + case edit(folder: BookmarkFolder, parentFolder: BookmarkFolder?) + } + + @Published var folderName: String + @Published var selectedFolder: BookmarkFolder? + + let folders: [FolderViewModel] + + var title: String { + mode.title + } + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + let isOtherActionDisabled = false + + var isDefaultActionDisabled: Bool { + folderName.trimmingWhitespace().isEmpty + } + + var addFolderPublisher: AnyPublisher { + addFolderSubject.eraseToAnyPublisher() + } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + private let addFolderSubject: PassthroughSubject = .init() + + init(mode: Mode, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { + self.mode = mode + self.bookmarkManager = bookmarkManager + folderName = mode.folderName + folders = .init(bookmarkManager.list) + selectedFolder = mode.parentFolder + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + defer { dismiss() } + + guard !folderName.isEmpty else { + assertionFailure("folderName is empty, button should be disabled") + return + } + + let folderName = folderName.trimmingWhitespace() + + switch mode { + case let .edit(folder, originalParent): + // If there are no pending changes dismiss + guard folder.title != folderName || selectedFolder?.id != originalParent?.id else { return } + // Otherwise update Folder. + update(folder: folder, originalParent: originalParent, newParent: selectedFolder) + case .add: + add(folderWithName: folderName, to: selectedFolder) + } + } + +} + +// MARK: - Private + +private extension AddEditBookmarkFolderDialogViewModel { + + func update(folder: BookmarkFolder, originalParent: BookmarkFolder?, newParent: BookmarkFolder?) { + // If the original location of the folder changed move it to the new folder. + if selectedFolder?.id != originalParent?.id { + // Update the title anyway. + folder.title = folderName + let parentFolderType: ParentFolderType = newParent.flatMap { ParentFolderType.parent(uuid: $0.id) } ?? .root + bookmarkManager.update(folder: folder, andMoveToParent: parentFolderType) + } else if folder.title != folderName { // If only title changed just update the folder title without updating its parent. + folder.title = folderName + bookmarkManager.update(folder: folder) + } + } + + func add(folderWithName name: String, to parent: BookmarkFolder?) { + bookmarkManager.makeFolder(for: name, parent: parent) { [weak self] bookmarkFolder in + self?.addFolderSubject.send(bookmarkFolder) + } + } + +} + +// MARK: - AddEditBookmarkFolderDialogViewModel.Mode + +private extension AddEditBookmarkFolderDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addFolder + case .edit: + return UserText.Bookmarks.Dialog.Title.editFolder + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addFolder + case .edit: + return UserText.save + } + } + + var folderName: String { + switch self { + case .add: + return "" + case let .edit(folder, _): + return folder.title + } + } + + var parentFolder: BookmarkFolder? { + switch self { + case let .add(parentFolder): + return parentFolder + case let .edit(_, parentFolder): + return parentFolder + } + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift new file mode 100644 index 0000000000..08ef2cc47d --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift @@ -0,0 +1,35 @@ +// +// BookmarksDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol BookmarksDialogViewModel: ObservableObject { + var title: String { get } + + var folders: [FolderViewModel] { get } + var selectedFolder: BookmarkFolder? { get set } + + var cancelActionTitle: String { get } + var isOtherActionDisabled: Bool { get } + var defaultActionTitle: String { get } + var isDefaultActionDisabled: Bool { get } + + func cancel(dismiss: () -> Void) + func addOrSave(dismiss: () -> Void) +} diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift index 61c0ec7630..dd9d2e617f 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift @@ -24,11 +24,13 @@ protocol BookmarksBarCollectionViewItemDelegate: AnyObject { func bookmarksBarCollectionViewItemOpenInNewTabAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemOpenInNewWindowAction(_ item: BookmarksBarCollectionViewItem) - func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemToggleFavoritesAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemMoveToEndAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemCopyBookmarkURLAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemDeleteEntityAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemAddEntityAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemManageBookmarksAction(_ item: BookmarksBarCollectionViewItem) } @@ -128,114 +130,72 @@ extension BookmarksBarCollectionViewItem: NSMenuDelegate { switch entityType { case .bookmark(_, _, _, let isFavorite): - menu.items = createBookmarkMenuItems(isFavorite: isFavorite) + menu.items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) case .folder: - menu.items = createFolderMenuItems() + menu.items = ContextualMenu.folderMenuItems() } } } -extension BookmarksBarCollectionViewItem { +extension BookmarksBarCollectionViewItem: BookmarkMenuItemSelectors { - // MARK: Bookmark Menu Items - - func createBookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { - let items = [ - openBookmarkInNewTabMenuItem(), - openBookmarkInNewWindowMenuItem(), - NSMenuItem.separator(), - addToFavoritesMenuItem(isFavorite: isFavorite), - editItem(), - moveToEndMenuItem(), - NSMenuItem.separator(), - copyBookmarkURLMenuItem(), - deleteEntityMenuItem() - ].compactMap { $0 } - - return items - } - - func openBookmarkInNewTabMenuItem() -> NSMenuItem { - return menuItem(UserText.openInNewTab, #selector(openBookmarkInNewTabMenuItemSelected(_:))) - } - - @objc - func openBookmarkInNewTabMenuItemSelected(_ sender: NSMenuItem) { + func openBookmarkInNewTab(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) } - func openBookmarkInNewWindowMenuItem() -> NSMenuItem { - return menuItem(UserText.openInNewWindow, #selector(openBookmarkInNewWindowMenuItemSelected(_:))) - } - - @objc - func openBookmarkInNewWindowMenuItemSelected(_ sender: NSMenuItem) { + func openBookmarkInNewWindow(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } - func addToFavoritesMenuItem(isFavorite: Bool) -> NSMenuItem? { - guard !isFavorite else { - return nil - } - - return menuItem(UserText.addToFavorites, #selector(addToFavoritesMenuItemSelected(_:))) - } - - @objc - func addToFavoritesMenuItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewItemAddToFavoritesAction(self) + func toggleBookmarkAsFavorite(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemToggleFavoritesAction(self) } - func editItem() -> NSMenuItem { - return menuItem("Edit…", #selector(editItemSelected(_:))) + func editBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewEditAction(self) } - @objc - func editItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewEditAction(self) + func copyBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) } - func moveToEndMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(moveToEndMenuItemSelected(_:))) + func deleteBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - @objc - func moveToEndMenuItemSelected(_ sender: NSMenuItem) { + func moveToEnd(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemMoveToEndAction(self) } - func copyBookmarkURLMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuCopy, #selector(copyBookmarkURLMenuItemSelected(_:))) + func deleteEntities(_ sender: NSMenuItem) {} + + func manageBookmarks(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemManageBookmarksAction(self) } - @objc - func copyBookmarkURLMenuItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) +} + +extension BookmarksBarCollectionViewItem: FolderMenuItemSelectors { + + func newFolder(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemAddEntityAction(self) } - func deleteEntityMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuDelete, #selector(deleteMenuItemSelected(_:))) + func editFolder(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewEditAction(self) } - @objc - func deleteMenuItemSelected(_ sender: NSMenuItem) { + func deleteFolder(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - // MARK: Folder Menu Items - - func createFolderMenuItems() -> [NSMenuItem] { - return [ - editItem(), - moveToEndMenuItem(), - NSMenuItem.separator(), - deleteEntityMenuItem() - ] + func openInNewTabs(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) } - func menuItem(_ title: String, _ action: Selector) -> NSMenuItem { - return NSMenuItem(title: title, action: action, keyEquivalent: "") + func openAllInNewWindow(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift index 56eff5f57a..c8d9f6c016 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift @@ -34,6 +34,13 @@ struct BookmarksBarMenuFactory { menu.addItem(makeMenuItem(prefs)) } + static func addToMenuWithManageBookmarksSection(_ menu: NSMenu, target: AnyObject, addFolderSelector: Selector, manageBookmarksSelector: Selector, prefs: AppearancePreferences = .shared) { + addToMenu(menu, prefs) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: UserText.addFolder, action: addFolderSelector, target: target)) + menu.addItem(NSMenuItem(title: UserText.bookmarksManageBookmarks, action: manageBookmarksSelector, target: target)) + } + private static func makeMenuItem( _ prefs: AppearancePreferences) -> NSMenuItem { let item = NSMenuItem(title: UserText.showBookmarksBar, action: nil, keyEquivalent: "B") item.submenu = NSMenu(items: [ diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index ea9b61e496..e298a6335e 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -223,59 +223,113 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { bookmarksBarCollectionView.reloadData() } - private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { +} + +// MARK: - Private + +private extension BookmarksBarViewController { + + func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { switch action { case .openInNewTab: - guard let url = bookmark.urlObject else { return } - tabCollectionViewModel.appendNewTab(with: .url(url, source: .bookmark), selected: true) + openInNewTab(bookmark: bookmark) case .openInNewWindow: - guard let url = bookmark.urlObject else { return } - WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: false) + openInNewWindow(bookmark: bookmark) case .clickItem: WindowControllersManager.shared.open(bookmark: bookmark) - case .addToFavorites: - bookmark.isFavorite = true + case .toggleFavorites: + bookmark.isFavorite.toggle() bookmarkManager.update(bookmark: bookmark) case .edit: - AddBookmarkModalView(model: AddBookmarkModalViewModel(originalBookmark: bookmark)) - .show(in: view.window) + showDialog(view: BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark)) case .moveToEnd: bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: .root) { _ in } case .copyURL: bookmark.copyUrlToPasteboard() case .deleteEntity: bookmarkManager.remove(bookmark: bookmark) + case .addFolder: + addFolder(inParent: nil) + case .manageBookmarks: + manageBookmarks() } } - private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { + func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { switch action { case .clickItem: - let childEntities = folder.children - let viewModels = childEntities.map { BookmarkViewModel(entity: $0) } - let menuItems = viewModel.bookmarksTreeMenuItems(from: viewModels, topLevel: true) - let menu = bookmarkFolderMenu(items: menuItems) - - menu.popUp(positioning: nil, at: CGPoint(x: 0, y: item.view.frame.minY - 7), in: item.view) + showSubmenuFor(folder: folder, fromView: item.view) case .edit: - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) - .show(in: view.window) + showDialog(view: BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: nil)) case .moveToEnd: bookmarkManager.move(objectUUIDs: [folder.id], toIndex: nil, withinParentFolder: .root) { _ in } case .deleteEntity: bookmarkManager.remove(folder: folder) + case .addFolder: + addFolder(inParent: folder) + case .openInNewTab: + openAllInNewTabs(folder: folder) + case .openInNewWindow: + openAllInNewWindow(folder: folder) + case .manageBookmarks: + manageBookmarks() default: assertionFailure("Received unexpected action for bookmark folder") } } - private func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { + func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { let menu = NSMenu() menu.items = items.isEmpty ? [NSMenuItem.empty] : items menu.autoenablesItems = false return menu } + func openInNewTab(bookmark: Bookmark) { + guard let url = bookmark.urlObject else { return } + tabCollectionViewModel.appendNewTab(with: .url(url, source: .bookmark), selected: true) + } + + func openInNewWindow(bookmark: Bookmark) { + guard let url = bookmark.urlObject else { return } + WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: false) + } + + func openAllInNewTabs(folder: BookmarkFolder) { + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollectionViewModel.burnerMode) + tabCollectionViewModel.append(tabs: tabs) + } + + func openAllInNewWindow(folder: BookmarkFolder) { + let tabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollectionViewModel.burnerMode) + WindowsManager.openNewWindow(with: tabCollection, isBurner: tabCollectionViewModel.isBurner) + } + + func showSubmenuFor(folder: BookmarkFolder, fromView view: NSView) { + let childEntities = folder.children + let viewModels = childEntities.map { BookmarkViewModel(entity: $0) } + let menuItems = viewModel.bookmarksTreeMenuItems(from: viewModels, topLevel: true) + let menu = bookmarkFolderMenu(items: menuItems) + + menu.popUp(positioning: nil, at: CGPoint(x: 0, y: view.frame.minY - 7), in: view) + } + + func addFolder(inParent parent: BookmarkFolder?) { + showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parent)) + } + + func showDialog(view: any ModalView) { + view.show(in: self.view.window) + } + + @objc func manageBookmarks() { + WindowControllersManager.shared.showBookmarksTab() + } + + @objc func addFolder(sender: NSMenuItem) { + addFolder(inParent: nil) + } + } // MARK: - Menu @@ -284,7 +338,12 @@ extension BookmarksBarViewController: NSMenuDelegate { public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - BookmarksBarMenuFactory.addToMenu(menu) + BookmarksBarMenuFactory.addToMenuWithManageBookmarksSection( + menu, + target: self, + addFolderSelector: #selector(addFolder(sender:)), + manageBookmarksSelector: #selector(manageBookmarks) + ) } } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift index 2dc81a1596..1c82b4c576 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift @@ -47,11 +47,13 @@ final class BookmarksBarViewModel: NSObject { case clickItem case openInNewTab case openInNewWindow - case addToFavorites + case toggleFavorites case edit case moveToEnd case copyURL case deleteEntity + case addFolder + case manageBookmarks } struct BookmarksBarItem { @@ -482,8 +484,8 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .openInNewWindow, for: item) } - func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) { - delegate?.bookmarksBarViewModelReceived(action: .addToFavorites, for: item) + func bookmarksBarCollectionViewItemToggleFavoritesAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .toggleFavorites, for: item) } func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) { @@ -502,4 +504,12 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .deleteEntity, for: item) } + func bookmarksBarCollectionViewItemAddEntityAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .addFolder, for: item) + } + + func bookmarksBarCollectionViewItemManageBookmarksAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .manageBookmarks, for: item) + } + } diff --git a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuExtension.swift index 69043e9a3b..6790d5c4f7 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuExtension.swift @@ -58,4 +58,14 @@ extension NSMenu { insertItem(newItem, at: index) } + /// Pops up the menu at the current mouse location. + /// + /// - Parameter view: The view to display the menu item over. + /// - Attention: If the view is not currently installed in a window, this function does not show any pop up menu. + func popUpAtMouseLocation(in view: NSView) { + guard let cursorLocation = view.window?.mouseLocationOutsideOfEventStream else { return } + let convertedLocation = view.convert(cursorLocation, from: nil) + popUp(positioning: nil, at: convertedLocation, in: view) + } + } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 555dece7ae..3c142fd364 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -426,7 +426,6 @@ struct UserText { static let addToFavorites = NSLocalizedString("add.to.favorites", value: "Add to Favorites", comment: "Button for adding bookmarks to favorites") static let addFavorite = NSLocalizedString("add.favorite", value: "Add Favorite", comment: "Button for adding a favorite bookmark") static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") - static let editFolder = NSLocalizedString("edit.folder", value: "Edit Folder", comment: "Header of the view that edits a bookmark folder") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page", comment: "Menu item for bookmarking current page") static let bookmarksShowToolbarPanel = NSLocalizedString("bookmarks.show-toolbar-panel", value: "Open Bookmarks Panel", comment: "Menu item for opening the bookmarks panel") @@ -459,7 +458,6 @@ struct UserText { static let newFolder = NSLocalizedString("folder.optionsMenu.newFolder", value: "New Folder", comment: "Option for creating a new folder") static let renameFolder = NSLocalizedString("folder.optionsMenu.renameFolder", value: "Rename Folder", comment: "Option for renaming a folder") static let deleteFolder = NSLocalizedString("folder.optionsMenu.deleteFolder", value: "Delete Folder", comment: "Option for deleting a folder") - static let newFolderDialogFolderNameTitle = NSLocalizedString("add.folder.name", value: "Name:", comment: "Add Folder popover: folder name text field title") static let newBookmarkDialogBookmarkNameTitle = NSLocalizedString("add.bookmark.name", value: "Name:", comment: "New bookmark folder dialog folder name field heading") static let updateBookmark = NSLocalizedString("bookmark.update", value: "Update Bookmark", comment: "Option for updating a bookmark") @@ -1048,9 +1046,9 @@ struct UserText { enum Bookmarks { enum Dialog { enum Title { - static let addBookmark = NSLocalizedString("bookmarks.dialog.title.add", value: "Add bookmark", comment: "Bookmark creation dialog title") + static let addBookmark = NSLocalizedString("bookmarks.dialog.title.add", value: "Add Bookmark", comment: "Bookmark creation dialog title") static let addedBookmark = NSLocalizedString("bookmarks.dialog.title.added", value: "Bookmark Added", comment: "Bookmark added popover title") - static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit bookmark", comment: "Bookmark edit dialog title") + static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit Bookmark", comment: "Bookmark edit dialog title") static let addFolder = NSLocalizedString("bookmarks.dialog.folder.title.add", value: "Add Folder", comment: "Bookmark folder creation dialog title") static let editFolder = NSLocalizedString("bookmarks.dialog.folder.title.edit", value: "Edit Folder", comment: "Bookmark folder edit dialog title") } diff --git a/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift b/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift index 0860f02cfc..e88a48b1b0 100644 --- a/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift @@ -86,14 +86,16 @@ extension HomePage.Models { let open: (Bookmark, OpenTarget) -> Void let removeFavorite: (Bookmark) -> Void let deleteBookmark: (Bookmark) -> Void - let addEdit: (Bookmark?) -> Void + let add: () -> Void + let edit: (Bookmark) -> Void let moveFavorite: (Bookmark, Int) -> Void let onFaviconMissing: () -> Void init(open: @escaping (Bookmark, OpenTarget) -> Void, removeFavorite: @escaping (Bookmark) -> Void, deleteBookmark: @escaping (Bookmark) -> Void, - addEdit: @escaping (Bookmark?) -> Void, + add: @escaping () -> Void, + edit: @escaping (Bookmark) -> Void, moveFavorite: @escaping (Bookmark, Int) -> Void, onFaviconMissing: @escaping () -> Void ) { @@ -102,7 +104,8 @@ extension HomePage.Models { self.open = open self.removeFavorite = removeFavorite self.deleteBookmark = deleteBookmark - self.addEdit = addEdit + self.add = add + self.edit = edit self.moveFavorite = moveFavorite self.onFaviconMissing = onFaviconMissing } @@ -119,12 +122,12 @@ extension HomePage.Models { open(bookmark, .current) } - func edit(_ bookmark: Bookmark) { - addEdit(bookmark) + func editBookmark(_ bookmark: Bookmark) { + edit(bookmark) } func addNew() { - addEdit(nil) + add() } private func updateVisibleModels() { diff --git a/DuckDuckGo/HomePage/View/FavoritesView.swift b/DuckDuckGo/HomePage/View/FavoritesView.swift index 90956a4a66..52e7f585d1 100644 --- a/DuckDuckGo/HomePage/View/FavoritesView.swift +++ b/DuckDuckGo/HomePage/View/FavoritesView.swift @@ -305,14 +305,17 @@ struct Favorite: View { let bookmark: Bookmark // Maintain separate copies of bookmark metadata required by the view, in order to ensure that SwiftUI re-renders correctly. + // Do not remove these properties even if some are not used in the `FavoriteTemplate` view as the view will not re-render correctly. private let bookmarkTitle: String private let bookmarkURL: URL + private let bookmarkParentFolder: String? init?(bookmark: Bookmark) { guard let urlObject = bookmark.urlObject else { return nil } self.bookmark = bookmark self.bookmarkTitle = bookmark.title self.bookmarkURL = urlObject + self.bookmarkParentFolder = bookmark.parentFolderUUID } var body: some View { diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index ae40aff5df..03f2cb4e08 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -172,8 +172,10 @@ final class HomePageViewController: NSViewController { self?.bookmarkManager.update(bookmark: bookmark) }, deleteBookmark: { [weak self] bookmark in self?.bookmarkManager.remove(bookmark: bookmark) - }, addEdit: { [weak self] bookmark in - self?.showAddEditController(for: bookmark) + }, add: { [weak self] in + self?.showAddController() + }, edit: { [weak self] bookmark in + self?.showEditController(for: bookmark) }, moveFavorite: { [weak self] (bookmark, index) in self?.bookmarkManager.moveFavorites(with: [bookmark.id], toIndex: index) { _ in } }, onFaviconMissing: { [weak self] in @@ -204,7 +206,7 @@ final class HomePageViewController: NSViewController { } func subscribeToBookmarks() { - bookmarkManager.listPublisher.receive(on: RunLoop.main).sink { [weak self] _ in + bookmarkManager.listPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in withAnimation { self?.refreshFavoritesModel() } @@ -230,9 +232,14 @@ final class HomePageViewController: NSViewController { tabCollectionViewModel.selectedTabViewModel?.tab.setContent(.contentFromURL(url, source: .bookmark)) } - private func showAddEditController(for bookmark: Bookmark? = nil) { - AddBookmarkModalView(model: AddBookmarkModalViewModel(originalBookmark: bookmark, isFavorite: true)) - .show(in: self.view.window) + private func showAddController() { + BookmarksDialogViewFactory.makeAddFavoriteView() + .show(in: view.window) + } + + private func showEditController(for bookmark: Bookmark) { + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + .show(in: view.window) } private var burningDataCancellable: AnyCancellable? diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index a00894c192..94ea695d1b 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -908,66 +908,6 @@ } } }, - "add.folder.name" : { - "comment" : "Add Folder popover: folder name text field title", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Name:" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Name:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nom :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naam:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nazwa:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Имя:" - } - } - } - }, "add.link.to.bookmarks" : { "comment" : "Context menu item", "extractionState" : "extracted_with_value", @@ -8959,7 +8899,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Add bookmark" + "value" : "Add Bookmark" } }, "es" : { @@ -9079,7 +9019,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Edit bookmark" + "value" : "Edit Bookmark" } }, "es" : { @@ -15626,66 +15566,6 @@ } } }, - "edit.folder" : { - "comment" : "Header of the view that edits a bookmark folder", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ordner bearbeiten" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Edit Folder" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar carpeta" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifier le dossier" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifica cartella" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Map bewerken" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edytuj folder" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar pasta" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изменить папку" - } - } - } - }, "email.copied" : { "comment" : "Notification that the Private email address was copied to clipboard after the user generated a new address", "extractionState" : "extracted_with_value", @@ -24584,59 +24464,6 @@ } } }, - "Location:" : { - "comment" : "Add Folder popover: parent folder picker title", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Emplacement :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posizione:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Locatie:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lokalizacja:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Localização:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Папка:" - } - } - } - }, "looking.for.bitwarden" : { "comment" : "Setup of the integration with Bitwarden app", "extractionState" : "extracted_with_value", @@ -47706,59 +47533,6 @@ } } }, - "Title:" : { - "comment" : "Add Bookmark dialog bookmark title field heading", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titre :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titolo:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tytuł:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Название:" - } - } - } - }, "tooltip.addToFavorites" : { "comment" : "Tooltip for add to favorites button", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift index 01b7b3f4ec..15e6bcdc66 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift @@ -286,19 +286,4 @@ private struct VPNLocationViewButtons: View { } -extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } -} - #endif diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialog.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/Dialog.swift similarity index 100% rename from LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialog.swift rename to LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/Dialog.swift diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift new file mode 100644 index 0000000000..541941304b --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift @@ -0,0 +1,70 @@ +// +// TieredDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view to arrange its subviews in a three vertical sections separated by dividers. +public struct TieredDialogView: View { + private let verticalSpacing: CGFloat + private let horizontalAlignment: HorizontalAlignment + private let horizontalPadding: CGFloat? + @ViewBuilder private let top: () -> Top + @ViewBuilder private let center: () -> Center + @ViewBuilder private let bottom: () -> Bottom + + /// Creates an instance with the given vertical spacing, horizontal alignment, horizontal padding and views created by the specified view builders. + /// - Parameters: + /// - verticalSpacing: The distance between adjacent sections. + /// - horizontalAlignment: The guide for aligning the sections in the vertical stack. This guide has the same vertical screen coordinate for every subview. + /// - horizontalPadding: The padding amount to add to the horizontal edges of the sections. + /// - top: A view builder that creates the content of the top section of the dialog. + /// - center: A view builder that creates the content of the central section of the dialog. + /// - bottom: A view builder that creates the content of the bottom section of the dialog. + public init( + verticalSpacing: CGFloat = 10.0, + horizontalAlignment: HorizontalAlignment = .leading, + horizontalPadding: CGFloat? = nil, + @ViewBuilder top: @escaping () -> Top, + @ViewBuilder center: @escaping () -> Center, + @ViewBuilder bottom: @escaping () -> Bottom + ) { + self.horizontalAlignment = horizontalAlignment + self.verticalSpacing = verticalSpacing + self.horizontalPadding = horizontalPadding + self.top = top + self.center = center + self.bottom = bottom + } + + public var body: some View { + VStack(alignment: horizontalAlignment, spacing: verticalSpacing) { + top() + .padding(.horizontal, horizontalPadding) + + Divider() + + center() + .padding(.horizontal, horizontalPadding) + + Divider() + + bottom() + .padding(.horizontal, horizontalPadding) + } + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift new file mode 100644 index 0000000000..55bb57bca0 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift @@ -0,0 +1,62 @@ +// +// TwoColumnsListView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view to arrange its subviews in two-column equally spaced rows. +public struct TwoColumnsListView: View { + private let rowHeight: CGFloat? + private let horizontalSpacing: CGFloat? + private let verticalSpacing: CGFloat? + @ViewBuilder private let leftColumn: () -> Left + @ViewBuilder private let rightColumn: () -> Right + + /// Creates an instance with the given horizontal and vertical spacing, row height and + /// - Parameters: + /// - horizontalSpacing: The horizontal distance between adjacent subviews. + /// - verticalSpacing: The vertical distance between adjacent subviews. + /// - rowHeight: The height of the rows in the stack. + /// - leftColumn: A view builder that creates the content of the left section of the view. + /// - rightColumn: A view builder that creates the content of the right section of the view. + public init( + horizontalSpacing: CGFloat? = nil, + verticalSpacing: CGFloat? = nil, + rowHeight: CGFloat? = nil, + @ViewBuilder leftColumn: @escaping () -> Left, + @ViewBuilder rightColumn: @escaping () -> Right + ) { + self.horizontalSpacing = horizontalSpacing + self.verticalSpacing = verticalSpacing + self.rowHeight = rowHeight + self.leftColumn = leftColumn + self.rightColumn = rightColumn + } + + public var body: some View { + HStack(alignment: .center, spacing: horizontalSpacing) { + VStack(alignment: .leading, spacing: verticalSpacing) { + leftColumn() + .frame(height: rowHeight) + } + VStack(alignment: .leading, spacing: verticalSpacing) { + rightColumn() + .frame(height: rowHeight) + } + } + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift new file mode 100644 index 0000000000..af223c4439 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift @@ -0,0 +1,47 @@ +// +// View+ConditionalModifiers.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +public extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + /// Applies the given transform if the given optional value is not `nil`. + /// - Parameters: + /// - value: The optional value to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the optional value is not `nil`. + @ViewBuilder func `ifLet`(_ value: Value?, transform: (Self, Value) -> Content) -> some View { + if let value = value { + transform(self, value) + } else { + self + } + } +} diff --git a/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift b/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift new file mode 100644 index 0000000000..5226ead0c6 --- /dev/null +++ b/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift @@ -0,0 +1,61 @@ +// +// Bookmarks+TabTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class Bookmarks_TabTests: XCTestCase { + + func testWhenBuildTabsWithContentOfFolderThenItShouldReturnAsManyTabsAsBookmarksWithinTheFolder() { + // GIVEN + let bookmark1 = Bookmark(id: "A", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let bookmark2 = Bookmark(id: "B", url: URL.atb, title: "ATB", isFavorite: false) + let subFolder = BookmarkFolder(id: "C", title: "Child") + let folder = BookmarkFolder(id: "1", title: "Test", children: [bookmark1, subFolder, bookmark2]) + + // WHEN + let tab = Tab.withContentOfBookmark(folder: folder, burnerMode: .regular) + + // THEN + XCTAssertEqual(tab.count, 2) + XCTAssertEqual(tab.first?.url?.absoluteString, bookmark1.url) + XCTAssertEqual(tab.first?.burnerMode, .regular) + XCTAssertEqual(tab.last?.url?.absoluteString, bookmark2.url) + XCTAssertEqual(tab.last?.burnerMode, .regular) + } + + func testWhenBuildTabCollectionWithContentOfFolderThenItShouldReturnACollectionWithAsManyTabsAsBookmarksWithinTheFolder() { + // GIVEN + let bookmark1 = Bookmark(id: "A", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let bookmark2 = Bookmark(id: "B", url: URL.atb, title: "ATB", isFavorite: false) + let subFolder = BookmarkFolder(id: "C", title: "Child") + let folder = BookmarkFolder(id: "1", title: "Test", children: [bookmark1, subFolder, bookmark2]) + + // WHEN + let tabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: .regular) + + // THEN + XCTAssertEqual(tabCollection.tabs.count, 2) + XCTAssertEqual(tabCollection.tabs.first?.url?.absoluteString, bookmark1.url) + XCTAssertEqual(tabCollection.tabs.first?.burnerMode, .regular) + XCTAssertEqual(tabCollection.tabs.last?.url?.absoluteString, bookmark2.url) + XCTAssertEqual(tabCollection.tabs.last?.burnerMode, .regular) + } + +} diff --git a/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift b/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift new file mode 100644 index 0000000000..df9f76ec4e --- /dev/null +++ b/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift @@ -0,0 +1,59 @@ +// +// BookmarksBarMenuFactoryTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarksBarMenuFactoryTests: XCTestCase { + + func testReturnAddFolderAndManageBookmarksWhenAddToMenuWithManageBookmarksSectionIsCalled() { + // GIVEN + let menu = NSMenu(title: "") + let targetMock = BookmarksBarTargetMock() + XCTAssertTrue(menu.items.isEmpty) + + // WHEN + BookmarksBarMenuFactory.addToMenuWithManageBookmarksSection(menu, target: targetMock, addFolderSelector: #selector(targetMock.addFolder(_:)), manageBookmarksSelector: #selector(targetMock.manageBookmarks)) + + // THEN + XCTAssertEqual(menu.items.count, 4) + XCTAssertEqual(menu.items[1].title, "") + XCTAssertNil(menu.items[1].action) + XCTAssertEqual(menu.items[2].title, UserText.addFolder) + XCTAssertEqual(menu.items[2].action, #selector(targetMock.addFolder(_:))) + XCTAssertEqual(menu.items[3].title, UserText.bookmarksManageBookmarks) + XCTAssertEqual(menu.items[3].action, #selector(targetMock.manageBookmarks)) + } + + func testShouldNotReturnAddFolderAndManageBookmarksWhenAddToMenuIsCalled() { + // GIVEN + let menu = NSMenu(title: "") + XCTAssertTrue(menu.items.isEmpty) + + // WHEN + BookmarksBarMenuFactory.addToMenu(menu) + + // THEN + XCTAssertEqual(menu.items.count, 1) + } +} + +private class BookmarksBarTargetMock: NSObject { + @objc func addFolder(_ sender: NSMenuItem) {} + @objc func manageBookmarks() {} +} diff --git a/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift new file mode 100644 index 0000000000..11ffefab31 --- /dev/null +++ b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift @@ -0,0 +1,247 @@ +// +// BaseBookmarkEntityTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BaseBookmarkEntityTests: XCTestCase { + + // MARK: - Folders + + func testTwoBookmarkFolderWithSamePropertiesReturnTrueWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkFolderWithDifferentIdReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFolderWithDifferentTitleReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child 1", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFolderWithDifferentParentReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: #function, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkParentFolderWithSameSubfoldersReturnTrueWhenIsEqualCalled() { + // GIVEN + let folder1 = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: "Parent", children: []) + let folder2 = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: "Parent", children: []) + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkParentFolderWithDifferentSubfoldersReturnFalseWhenIsEqualCalled() { + // GIVEN + let folder1 = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: "Parent", children: []) + let folder2 = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: "Parent", children: []) + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2, BookmarkFolder(id: "3", title: "")]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkParentFolderWithSameBookmarksReturnTrueWhenIsEqualCalled() { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let bookmark2 = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkParentFolderWithDifferentBookmarksReturnFalseWhenIsEqualCalled() { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let bookmark2 = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2, BookmarkFolder(id: "4", title: "New")]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Bookmarks + + func testTwoBookmarkWithSamePropertiesReturnTrueWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: parentFolder.id) + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: parentFolder.id) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkWithDifferentIdReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentURLReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.devMode, title: "DDG", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentTitleReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG 2", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentIsFavoriteReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentParentFolderReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "z-a") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFoldersAddedToRootFolderReturnTrueWhenLeftParentIsBookmarksRootAndRightIsNil() { + // GIVEN + let lhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: "bookmarks_root", children: []) + let rhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: nil, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkFoldersAddedToRootFolderReturnTrueWhenLeftParentIsNilAndRightParentIsRootBookmarks() { + // GIVEN + let lhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: nil, children: []) + let rhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: "bookmarks_root", children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + // MARK: - Base Entity + + func testDifferentBookmarkEntitiesReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhs = BookmarkFolder(id: "1", title: "DDG") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + +} diff --git a/UnitTests/Bookmarks/Model/BookmarkListTests.swift b/UnitTests/Bookmarks/Model/BookmarkListTests.swift index e959ff9878..f0f8cda903 100644 --- a/UnitTests/Bookmarks/Model/BookmarkListTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkListTests.swift @@ -23,15 +23,20 @@ import XCTest final class BookmarkListTests: XCTestCase { - func testWhenBookmarkIsInserted_ThenItIsPartOfTheList() { + func testWhenBookmarkIsInserted_ThenItIsPartOfTheList() throws { var bookmarkList = BookmarkList() let bookmark = Bookmark.aBookmark bookmarkList.insert(bookmark) + let result = try XCTUnwrap(bookmarkList[bookmark.url]) XCTAssert(bookmarkList.bookmarks().count == 1) XCTAssert((bookmarkList.bookmarks()).first == bookmark.identifiableBookmark) - XCTAssertNotNil(bookmarkList[bookmark.url]) + XCTAssertEqual(result.id, bookmark.id) + XCTAssertEqual(result.title, bookmark.title) + XCTAssertEqual(result.url, bookmark.url) + XCTAssertEqual(result.isFavorite, bookmark.isFavorite) + XCTAssertEqual(result.parentFolderUUID, bookmark.parentFolderUUID) } func testWhenBookmarkIsAlreadyPartOfTheListInserted_ThenItCantBeInserted() { @@ -93,7 +98,7 @@ final class BookmarkListTests: XCTestCase { XCTAssertNil(updateUrlResult) } - func testWhenBookmarkUrlIsUpdated_ThenJustTheBookmarkUrlIsUpdated() { + func testWhenBookmarkUrlIsUpdated_ThenJustTheBookmarkUrlIsUpdated() throws { var bookmarkList = BookmarkList() let bookmarks = [ @@ -104,11 +109,14 @@ final class BookmarkListTests: XCTestCase { bookmarks.forEach { bookmarkList.insert($0) } let bookmarkToReplace = bookmarks[2] - let newBookmark = bookmarkList.updateUrl(of: bookmarkToReplace, to: URL.duckDuckGoAutocomplete.absoluteString) + let newBookmark = try XCTUnwrap(bookmarkList.updateUrl(of: bookmarkToReplace, to: URL.duckDuckGoAutocomplete.absoluteString)) + let result = try XCTUnwrap(bookmarkList[newBookmark.url]) XCTAssert(bookmarkList.bookmarks().count == bookmarks.count) XCTAssertNil(bookmarkList[bookmarkToReplace.url]) - XCTAssertNotNil(bookmarkList[newBookmark!.url]) + XCTAssertEqual(result.title, "Title") + XCTAssertEqual(result.url, URL.duckDuckGoAutocomplete.absoluteString) + XCTAssertTrue(result.isFavorite) } func testWhenBookmarkUrlIsUpdatedToAlreadyBookmarkedUrl_ThenUpdatingMustFail() { @@ -127,10 +135,51 @@ final class BookmarkListTests: XCTestCase { XCTAssert(bookmarkList.bookmarks().count == bookmarks.count) XCTAssertNotNil(bookmarkList[firstUrl.absoluteString]) + XCTAssertEqual(bookmarkList[firstUrl.absoluteString]?.url, firstUrl.absoluteString) XCTAssertNotNil(bookmarkList[bookmarkToReplace.url]) + XCTAssertEqual(bookmarkList[bookmarkToReplace.url]?.url, URL.duckDuckGo.absoluteString) XCTAssertNil(newBookmark) } + func testWhenBookmarkURLTitleAndIsFavoriteIsUpdated_ThenURLTitleAndIsFavoriteIsUpdated() throws { + // GIVEN + var bookmarkList = BookmarkList() + let bookmarks = [ + Bookmark(id: UUID().uuidString, url: "wikipedia.org", title: "Wikipedia", isFavorite: true), + Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true), + Bookmark(id: UUID().uuidString, url: "apple.com", title: "Apple", isFavorite: true) + ] + bookmarks.forEach { bookmarkList.insert($0) } + let bookmarkToReplace = bookmarks[2] + XCTAssertEqual(bookmarkList.bookmarks().count, bookmarks.count) + XCTAssertEqual(bookmarkList["wikipedia.org"]?.url, "wikipedia.org") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.title, "Wikipedia") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.isFavorite, true) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.title, "DDG") + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.isFavorite, true) + XCTAssertEqual(bookmarkList["apple.com"]?.url, "apple.com") + XCTAssertEqual(bookmarkList["apple.com"]?.title, "Apple") + XCTAssertEqual(bookmarkList["apple.com"]?.isFavorite, true) + + // WHEN + let newBookmark = try XCTUnwrap(bookmarkList.update(bookmark: bookmarkToReplace, newURL: "www.example.com", newTitle: "Example", newIsFavorite: false)) + + // THEN + let result = try XCTUnwrap(bookmarkList[newBookmark.url]) + XCTAssertEqual(bookmarkList.bookmarks().count, bookmarks.count) + XCTAssertNil(bookmarkList[bookmarkToReplace.url]) + XCTAssertEqual(bookmarkList["wikipedia.org"]?.url, "wikipedia.org") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.title, "Wikipedia") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.isFavorite, true) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.title, "DDG") + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.isFavorite, true) + XCTAssertEqual(result.url, "www.example.com") + XCTAssertEqual(result.title, "Example") + XCTAssertEqual(result.isFavorite, false) + } + } fileprivate extension Bookmark { diff --git a/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift b/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift index 8a9060c2f9..fd795b2ebb 100644 --- a/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift @@ -217,4 +217,174 @@ class BookmarkNodeTests: XCTestCase { XCTAssertNotEqual(rootNode.findOrCreateChildNode(with: TestObject()), childNode) } + // MARK: - Equality Bookmarks + + func testWhenTwoNodesWithSameIdAndSameBookmarkAsRepresentedObject_ThenIsEqualShouldBeTrue() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertTrue(result) + } + + func testWhenTwoNodesWithDifferentIdAndSameBookmarkAsRepresentedObject_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 2) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentId_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentURL_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.ddgLearnMore.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentTitle_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG 2", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentIsFavorite_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesWithSameIdAndSameFolderAsRepresentedObject_ThenIsEqualShouldBeTrue() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertTrue(result) + } + + func testWhenTwoNodesWithDifferentIdAndSameFolderAsRepresentedObject_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 2) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentId_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "2", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentName_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder 1", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentParentFolder_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "2", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentChildren_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: [Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true)]) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "2", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + } diff --git a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift index 8595ebdcc7..d1cc5fa4d8 100644 --- a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift @@ -110,7 +110,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let mockDestinationNode = treeController.node(representing: mockDestinationFolder)! let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) - let pasteboardFolder = PasteboardFolder(id: UUID().uuidString, name: "Pasteboard Folder") + let pasteboardFolder = PasteboardFolder(folder: .init(id: UUID().uuidString, title: "Pasteboard Folder")) let result = dataSource.validateDrop(for: [pasteboardFolder], destination: mockDestinationNode) XCTAssertEqual(result, .move) @@ -130,7 +130,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) let mockDestinationNode = treeController.node(representing: mockDestinationFolder)! - let pasteboardFolder = PasteboardFolder(id: mockDestinationFolder.id, name: "Pasteboard Folder") + let pasteboardFolder = PasteboardFolder(folder: mockDestinationFolder) let result = dataSource.validateDrop(for: [pasteboardFolder], destination: mockDestinationNode) XCTAssertEqual(result, .none) @@ -153,12 +153,64 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let mockDestinationNode = treeController.node(representing: childFolder)! // Simulate dragging the root folder onto the child folder: - let draggedFolder = PasteboardFolder(id: rootFolder.id, name: "Root") + let draggedFolder = PasteboardFolder(folder: rootFolder) let result = dataSource.validateDrop(for: [draggedFolder], destination: mockDestinationNode) XCTAssertEqual(result, .none) } + func testWhenCellFiresDelegate_ThenOnMenuRequestedActionShouldFire() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + var didFireClosure = false + var capturedCell: BookmarkOutlineCellView? + let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: LocalBookmarkManager(), treeController: treeController) { cell in + didFireClosure = true + capturedCell = cell + } + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // WHEN + cell.delegate?.outlineCellViewRequestedMenu(cell) + + // THEN + XCTAssertTrue(didFireClosure) + XCTAssertEqual(cell, capturedCell) + } + + func testWhenShowMenuButtonOnHoverIsTrue_ThenCellShouldHaveShouldMenuButtonFlagTrue() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + let dataSource = BookmarkOutlineViewDataSource(contentMode: .bookmarksAndFolders, bookmarkManager: LocalBookmarkManager(), treeController: treeController, showMenuButtonOnHover: true) + + // WHEN + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // THEN + XCTAssertTrue(cell.shouldShowMenuButton) + } + + func testWhenShowMenuButtonOnHoverIsFalse_ThenCellShouldHaveShouldMenuButtonFlagFalse() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: LocalBookmarkManager(), treeController: treeController, showMenuButtonOnHover: false) + + // WHEN + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // THEN + XCTAssertFalse(cell.shouldShowMenuButton) + } + // MARK: - Private private func createTreeController(with bookmarks: [BaseBookmarkEntity]) -> BookmarkTreeController { diff --git a/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift b/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift index 6c9e8c59da..4b95d7ca90 100644 --- a/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift @@ -28,24 +28,18 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let defaultNodes = treeController.rootNode.childNodes let representedObjects = defaultNodes.representedObjects() - // The sidebar defines three hardcoded nodes: + // The sidebar defines one hardcoded nodes: // - // 1. Favorites node - // 2. Spacer node - // 3. Bookmarks node + // 1. Bookmarks node - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - XCTAssertFalse(defaultNodes[0].canHaveChildNodes) - XCTAssertFalse(defaultNodes[1].canHaveChildNodes) - XCTAssertTrue(defaultNodes[2].canHaveChildNodes) + XCTAssertTrue(defaultNodes[0].canHaveChildNodes) - XCTAssert(representedObjects[0] === PseudoFolder.favorites) - XCTAssert(representedObjects[1] === SpacerNode.blank) - XCTAssert(representedObjects[2] === PseudoFolder.bookmarks) + XCTAssert(representedObjects.first === PseudoFolder.bookmarks) } - func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() { + func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() throws { let bookmarkStoreMock = BookmarkStoreMock() let faviconManagerMock = FaviconManagerMock() let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) @@ -56,11 +50,13 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) // The sidebar tree controller only shows folders, so if there are only bookmarks then the bookmarks default folder will be empty. - let bookmarksNode = defaultNodes[2] - XCTAssert(bookmarksNode.childNodes.isEmpty) + let bookmarksNode = defaultNodes[0] + let pseudoFolder = try XCTUnwrap(bookmarksNode.representedObject as? PseudoFolder) + XCTAssertTrue(bookmarksNode.childNodes.isEmpty) + XCTAssertEqual(pseudoFolder.name, "Bookmarks") } func testWhenBookmarkStoreHasTopLevelFolders_ThenTheDefaultBookmarksNodeHasThemAsChildren() { @@ -75,9 +71,9 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - let bookmarksNode = defaultNodes[2] + let bookmarksNode = defaultNodes[0] XCTAssertEqual(bookmarksNode.childNodes.count, 1) let childNode = bookmarksNode.childNodes[0] @@ -98,9 +94,9 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - let bookmarksNode = defaultNodes[2] + let bookmarksNode = defaultNodes[0] XCTAssertTrue(bookmarksNode.canHaveChildNodes) XCTAssertEqual(bookmarksNode.childNodes.count, 1) diff --git a/UnitTests/Bookmarks/Model/BookmarkTests.swift b/UnitTests/Bookmarks/Model/BookmarkTests.swift index cd21752a06..e6cf1df214 100644 --- a/UnitTests/Bookmarks/Model/BookmarkTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkTests.swift @@ -90,7 +90,7 @@ class BookmarkTests: XCTestCase { XCTAssertEqual(folder.childFolders.count, 0) XCTAssertEqual(folder.childBookmarks.count, 1) XCTAssertEqual(folder.children, [ - BaseBookmarkEntity.from(managedObject: bookmarkManagedObject, favoritesDisplayMode: .displayNative(.desktop)) + BaseBookmarkEntity.from(managedObject: bookmarkManagedObject, parentFolderUUID: folder.id, favoritesDisplayMode: .displayNative(.desktop)) ]) XCTAssertNil(folder.parentFolderUUID) diff --git a/UnitTests/Bookmarks/Model/ContextualMenuTests.swift b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift new file mode 100644 index 0000000000..89da2c186a --- /dev/null +++ b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift @@ -0,0 +1,267 @@ +// +// ContextualMenuTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class ContextualMenuTests: XCTestCase { + + func testWhenAskingBookmarkMenuItemsAndIsNotFavoriteThenItShouldReturnTheItemsInTheCorrectOrder() { + // GIVEN + let isFavorite = false + + // WHEN + let items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) + + // THEN + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:))) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:))) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:))) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:))) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + + } + + func testWhenAskingBookmarkMenuItemsAndIsFavoriteThenItShouldReturnTheItemsInTheCorrectOrder() { + // GIVEN + let isFavorite = true + + // WHEN + let items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) + + // THEN + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:))) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.removeFromFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:))) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:))) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:))) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenAskingFolderItemThenItShouldReturnTheItemsInTheCorrectOrders() { + // WHEN + let items = ContextualMenu.folderMenuItems() + + // THEN + XCTAssertEqual(items.count, 9) + assertMenu(item: items[0], withTitle: UserText.openAllInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:))) + assertMenu(item: items[1], withTitle: UserText.openAllTabsInNewWindow, selector: #selector(FolderMenuItemSelectors.openAllInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.editBookmark, selector: #selector(FolderMenuItemSelectors.editFolder(_:))) + assertMenu(item: items[4], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(FolderMenuItemSelectors.deleteFolder(_:))) + assertMenu(item: items[5], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(FolderMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[6], withTitle: "", selector: nil) // Separator + assertMenu(item: items[7], withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(FolderMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForEmptySelectionThenItReturnsAMenuWithAddFolderOnly() throws { + // WHEN + let menu = ContextualMenu.menu(for: []) + + // THEN + XCTAssertEqual(menu?.items.count, 1) + let menuItem = try XCTUnwrap(menu?.items.first) + assertMenu(item: menuItem, withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:))) + } + + func testWhenCreateMenuForBookmarkWithoutParentThenReturnsAMenuWithTheBookmarkMenuItems() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), representedObject: bookmark) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), representedObject: bookmark) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: bookmark) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: nil)) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForBookmarkWithParentThenReturnsAMenuWithTheBookmarkMenuItems() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "A") + let parent = BookmarkFolder(id: "A", title: "Folder", children: [bookmark]) + let parentNode = BookmarkNode(representedObject: parent, parent: nil) + let node = BookmarkNode(representedObject: bookmark, parent: parentNode) + + // WHEN + let menu = ContextualMenu.menu(for: [node]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), representedObject: bookmark) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), representedObject: bookmark) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: bookmark) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: parent)) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:)), representedObject: parent) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForFolderNodeThenReturnsAMenuWithTheFolderMenuItems() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Child") + let parent = BookmarkFolder(id: "1", title: "Parent", children: [folder]) + let parentNode = BookmarkNode(representedObject: parent, parent: nil) + let node = BookmarkNode(representedObject: folder, parent: parentNode) + + // WHEN + let menu = ContextualMenu.menu(for: [node]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 9) + assertMenu(item: items[0], withTitle: UserText.openAllInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: folder) + assertMenu(item: items[1], withTitle: UserText.openAllTabsInNewWindow, selector: #selector(FolderMenuItemSelectors.openAllInNewWindow(_:)), representedObject: folder) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.editBookmark, selector: #selector(FolderMenuItemSelectors.editFolder(_:)), representedObject: BookmarkEntityInfo(entity: folder, parent: parent)) + assertMenu(item: items[4], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(FolderMenuItemSelectors.deleteFolder(_:)), representedObject: folder) + assertMenu(item: items[5], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(FolderMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: folder, parent: parent)) + assertMenu(item: items[6], withTitle: "", selector: nil) // Separator + assertMenu(item: items[7], withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:)), representedObject: folder) + assertMenu(item: items[8], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(FolderMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForMultipleUnfavoriteBookmarksThenReturnsMenuWithOpenInNewTabsAddToFavoritesAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: false) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 4) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForMultipleFavoriteBookmarksThenReturnsMenuWithOpenInNewTabsRemoveFromFavoritesAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: true) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: true) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 4) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: UserText.removeFromFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForMultipleMixedFavoriteBookmarksThenReturnsMenuWithOpenInNewTabsAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: true) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 3) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: "", selector: nil) // Separator + assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForBookmarkAndFolderThenReturnsMenuWithOpenInNewTabsOnlyForBookmarkAndDelete() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: "", title: "Bookmark", isFavorite: true) + let folder = BookmarkFolder(id: "1", title: "Folder") + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark, folder]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 3) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark]) + assertMenu(item: items[1], withTitle: "", selector: nil) // Separator + assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark, folder]) + } + +} + +private extension ContextualMenuTests { + + func assertMenu(item: NSMenuItem, withTitle title: String, selector: Selector?, representedObject: T = Empty() ) { + XCTAssertEqual(item.title, title) + XCTAssertEqual(item.action, selector) + if representedObject is Empty { + XCTAssertNil(item.representedObject) + } else { + XCTAssertEqualValue(item.representedObject, representedObject) + } + } + +} + +private struct Empty: Equatable {} + +private func XCTAssertEqualValue(_ expression1: @autoclosure () throws -> Any?, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where T: Equatable { + do { + guard let firstValue = try expression1() as? T else { + XCTFail("Type of expression1 \(type(of: try? expression1())) and expression2 \(type(of: try? expression2())) are different.") + return + } + let secondValue = try expression2() + XCTAssertEqual(firstValue, secondValue, message(), file: file, line: line) + } catch { + XCTFail("Failed evaluating expression.") + } +} diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index b82636a7c5..97ce23202d 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Combine import Foundation import XCTest @@ -157,6 +158,26 @@ final class LocalBookmarkManagerTests: XCTestCase { XCTAssert(bookmarkStoreMock.updateBookmarkCalled) } + func testWhenBookmarkFolderIsUpdatedAndMoved_ThenManagerUpdatesItAlsoInStore() throws { + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + let parent = BookmarkFolder(id: "1", title: "Parent") + let folder = BookmarkFolder(id: "2", title: "Child") + var bookmarkList: BookmarkList? + let cancellable = bookmarkManager.listPublisher + .dropFirst() + .sink { list in + bookmarkList = list + } + + bookmarkManager.update(folder: folder, andMoveToParent: .parent(uuid: parent.id)) + + withExtendedLifetime(cancellable) {} + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: parent.id)) + XCTAssertNotNil(bookmarkList) + } + } fileprivate extension LocalBookmarkManager { diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 0dff288b23..2ebff62e7d 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -56,7 +56,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let bookmark = Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + let bookmark = Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "bookmarks_root") bookmarkStore.save(bookmark: bookmark, parent: nil, index: nil) { (success, error) in XCTAssert(success) @@ -128,7 +128,7 @@ final class LocalBookmarkStoreTests: XCTestCase { savingExpectation.fulfill() - let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo.absoluteString, title: "New Title", isFavorite: false) + let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo.absoluteString, title: "New Title", isFavorite: false, parentFolderUUID: "bookmarks_root") bookmarkStore.update(bookmark: modifiedBookmark) bookmarkStore.loadAll(type: .bookmarks) { bookmarks, error in @@ -152,7 +152,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let folder = BookmarkFolder(id: UUID().uuidString, title: "Folder") + let folder = BookmarkFolder(id: UUID().uuidString, title: "Folder", parentFolderUUID: "bookmarks_root") bookmarkStore.save(folder: folder, parent: nil) { (success, error) in XCTAssert(success) @@ -181,8 +181,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let saveChildExpectation = self.expectation(description: "Save Child Folder") let loadingExpectation = self.expectation(description: "Loading") - let parentFolder = BookmarkFolder(id: UUID().uuidString, title: "Parent") - let childFolder = BookmarkFolder(id: UUID().uuidString, title: "Child") + let parentId = UUID().uuidString + let childFolder = BookmarkFolder(id: UUID().uuidString, title: "Child", parentFolderUUID: parentId) + let parentFolder = BookmarkFolder(id: parentId, title: "Parent", parentFolderUUID: "bookmarks_root", children: [childFolder]) bookmarkStore.save(folder: parentFolder, parent: nil) { (success, error) in XCTAssert(success) @@ -224,8 +225,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let saveBookmarkExpectation = self.expectation(description: "Save Bookmark") let loadingExpectation = self.expectation(description: "Loading") - let folder = BookmarkFolder(id: UUID().uuidString, title: "Parent") - let bookmark = Bookmark(id: UUID().uuidString, url: "https://example.com", title: "Example", isFavorite: false) + let parentId = UUID().uuidString + let bookmark = Bookmark(id: UUID().uuidString, url: "https://example.com", title: "Example", isFavorite: false, parentFolderUUID: parentId) + let folder = BookmarkFolder(id: parentId, title: "Parent", parentFolderUUID: "bookmarks_root", children: [bookmark]) bookmarkStore.save(folder: folder, parent: nil) { (success, error) in XCTAssert(success) @@ -468,6 +470,148 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(topLevelEntityIDs, [testState.initialParentFolder.id, testState.bookmark3.id]) } + func testWhenUpdatingBookmarkFolder_ThenBookmarkFolderTitleIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folders.first, folder1) + + // Update the folder title and parent: + + let folderToMove = folder1 + folderToMove.title = #function + bookmarkStore.update(folder: folder1) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 1) + XCTAssertEqual(newFolders.first, folderToMove) + } + + func testWhenUpdatingAndMovingBookmarkFolder_ThenBookmarkFolderIsMovedAndTitleUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2", parentFolderUUID: "bookmarks_root") + let folder3 = BookmarkFolder(id: UUID().uuidString, title: "Folder 3", parentFolderUUID: "bookmarks_root") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + _ = await bookmarkStore.save(folder: folder2, parent: nil) + _ = await bookmarkStore.save(folder: folder3, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 3) + XCTAssertEqual(folders[0], folder1) + XCTAssertEqual(folders[1], folder2) + XCTAssertEqual(folders[2], folder3) + + // Update the folder title and parent: + + let folderToMove = folder1 + folderToMove.title = #function + bookmarkStore.update(folder: folder1, andMoveToParent: .parent(uuid: folder2.id)) + let expectedFolderAfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: folder2.id, children: folder1.children) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 2) + XCTAssertEqual(newFolders[0].id, folder2.id) + XCTAssertEqual(newFolders[0].children, [expectedFolderAfterMove]) + XCTAssertEqual(newFolders[1], folder3) + } + + func testWhenMovingBookmarkFolderToSubfolder_ThenBookmarkFolderLocationIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2", parentFolderUUID: "bookmarks_root") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + _ = await bookmarkStore.save(folder: folder2, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 2) + XCTAssertEqual(folders.first, folder1) + XCTAssertEqual(folders.last, folder2) + + // Update the folder parent: + + _ = await bookmarkStore.move(objectUUIDs: [folder2.id], toIndex: nil, withinParentFolder: .parent(uuid: folder1.id)) + let expectedChildFolderAfterMove = BookmarkFolder(id: folder2.id, title: folder2.title, parentFolderUUID: folder1.id, children: folder2.children) + let expectedParentFolderAfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: folder1.parentFolderUUID, children: [expectedChildFolderAfterMove]) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 1) + XCTAssertEqual(newFolders.first, expectedParentFolderAfterMove) + XCTAssertEqual(newFolders.first?.children, [expectedChildFolderAfterMove]) + } + + func testWhenMovingBookmarkFolderToRootFolder_ThenBookmarkFolderLocationIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder2Id = UUID().uuidString + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: folder2Id) + let folder2 = BookmarkFolder(id: folder2Id, title: "Folder 2", parentFolderUUID: "bookmarks_root", children: [folder1]) + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder2, parent: nil) + _ = await bookmarkStore.save(folder: folder1, parent: folder2) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folders.first, folder2) + XCTAssertEqual(folders.first?.children, [folder1]) + + // Update the folder parent: + + _ = await bookmarkStore.move(objectUUIDs: [folder1.id], toIndex: 0, withinParentFolder: .root) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + let expectedFolder1AfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: "bookmarks_root", children: folder1.children) + let expectedFolder2AfterMove = BookmarkFolder(id: folder2.id, title: folder2.title, parentFolderUUID: "bookmarks_root", children: []) + + XCTAssertEqual(newFolders.count, 2) + XCTAssertEqual(newFolders.first, expectedFolder1AfterMove) + XCTAssertEqual(newFolders.last, expectedFolder2AfterMove) + XCTAssertEqual(newFolders.last?.children, []) + } + // MARK: Favorites func testThatTopLevelEntitiesDoNotContainFavoritesFolder() async { diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift new file mode 100644 index 0000000000..53072513c4 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift @@ -0,0 +1,170 @@ +// +// AddEditBookmarkDialogCoordinatorViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogCoordinatorViewModelTests: XCTestCase { + private var sut: AddEditBookmarkDialogCoordinatorViewModel! + private var bookmarkViewModelMock: AddEditBookmarkDialogViewModelMock! + private var bookmarkFolderViewModelMock: AddEditBookmarkFolderDialogViewModelMock! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + bookmarkViewModelMock = .init() + bookmarkFolderViewModelMock = .init() + sut = .init(bookmarkModel: bookmarkViewModelMock, folderModel: bookmarkFolderViewModelMock) + } + + override func tearDownWithError() throws { + cancellables = nil + bookmarkViewModelMock = nil + bookmarkFolderViewModelMock = nil + sut = nil + try super.tearDownWithError() + } + + func testShouldReturnViewStateBookmarkWhenInit() { + XCTAssertEqual(sut.viewState, .bookmark) + } + + func testShouldReturnViewStateBookmarkWhenDismissActionIsCalled() { + // GIVEN + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .folder) + + // WHEN + sut.dismissAction() + + // THEN + XCTAssertEqual(sut.viewState, .bookmark) + + } + + func testShouldSetSelectedFolderOnFolderViewModelAndReturnFolderViewStateWhenAddFolderActionIsCalled() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkViewModelMock.selectedFolder = folder + XCTAssertNil(bookmarkFolderViewModelMock.selectedFolder) + + // WHEN + sut.addFolderAction() + + // THEN + XCTAssertEqual(bookmarkFolderViewModelMock.selectedFolder, folder) + } + + func testShouldReceiveEventsWhenBookmarkModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.bookmarkModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldReceiveEventsWhenBookmarkFolderModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.folderModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldSetSelectedFolderOnBookmarkViewModelWhenAddFolderPublisherSendsEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + bookmarkViewModelMock.selectedFolderExpectation = expectation + let folder = BookmarkFolder(id: "ABCDE", title: #function) + XCTAssertNil(bookmarkViewModelMock.selectedFolder) + + // WHEN + sut.folderModel.subject.send(folder) + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertEqual(bookmarkViewModelMock.selectedFolder, folder) + } + +} + +final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { + var bookmarkName: String = "" + var bookmarkURLPath: String = "" + var isBookmarkFavorite: Bool = false + var isURLFieldHidden: Bool = false + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} + +final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { + let subject = PassthroughSubject() + + var addFolderPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var folderName: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift new file mode 100644 index 0000000000..b409df5b9c --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -0,0 +1,745 @@ +// +// AddEditBookmarkDialogViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addBookmark) + } + + func testReturnEditBookmarkTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editBookmark) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addBookmark) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetBookmarkNameToEmptyWhenInitModeIsAddAndTabInfoIsNil() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetNameAndURLToValueWhenInitModeIsAddTabInfoIsNotNilAndURLIsNotAlreadyBookmarked() { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + let url = sut.bookmarkURLPath + + // THEN + XCTAssertEqual(name, "Test") + XCTAssertEqual(url, URL.duckDuckGo.absoluteString) + } + + func testShouldSetNameAndURLToEmptyWhenInitModeIsAddTabInfoIsNotNilAndURLIsAlreadyBookmarked() throws { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let websiteInfo = try XCTUnwrap(WebsiteInfo(tab)) + let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title ?? "", isFavorite: false) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + let url = sut.bookmarkURLPath + + // THEN + XCTAssertEqual(name, "") + XCTAssertEqual(url, "") + } + + func testShouldSetBookmarkNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenBookmarkParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "1") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testShouldSetIsBookmarkFavoriteToTrueWhenModeIsAddAndShouldPresetFavoriteIsTrue() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: true), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isBookmarkFavorite + + // THEN + XCTAssertTrue(result) + } + + func testShouldNotSetIsBookmarkFavoriteToTrueWhenModeIsAddAndShouldPresetFavoriteIsFalse() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: false), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isBookmarkFavorite + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarURLIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkURLIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + var didCallDismiss = false + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveBookmarkWhenModeIsAddAndURLIsNotAnExistingBookmark() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let existingBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + bookmarkStoreMock.bookmarks = [existingBookmark] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DDG" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [existingBookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, "DDG") + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenModeIsAddAndURLIsAnExistingBookmark() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = #function + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, #function) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenURLIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = expectedBookmark.url + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenURLIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenNameIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = expectedBookmark.title + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenNameIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = #function + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = false + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenURLAndTitleAndIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: "DDG", isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkURLPath = expectedBookmark.url + sut.bookmarkName = expectedBookmark.title + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder") + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + } + + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNilAndOriginalFolderIsNotRootFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNotDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNilAndOriginalFolderIsRootAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "bookmarks_root") + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift new file mode 100644 index 0000000000..a48f18d2ab --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -0,0 +1,425 @@ +// +// AddEditBookmarkFolderDialogViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkFolderTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addFolder) + } + + func testReturnEditBookmarkFolderTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editFolder) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkFolderActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addFolder) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetFolderNameToEmptyWhenInitAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetFolderNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, .mock) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, .mock) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenFolderNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenFolderNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenFolderNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.folderName = " Test " + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenFolderNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = " Test " + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + sut.folderName = "DuckDuckGo" + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveFolderWhenAddOrSaveIsCalledAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.folderName = #function + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertTrue(bookmarkStoreMock.saveFolderCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateFolderWhenNameIsChanged() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = "TEST" + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertTrue(bookmarkStoreMock.updateFolderCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + } + + func testShouldNotAskBookmarkStoreToUpdateFolderWhenNameIsNotChanged() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder?.title) + } + + func testShouldAskBookmarkStoreToMoveFolderToSubfolderWhenSelectedFolderIsDifferentFromOriginalFolder() { + // GIVEN + let location = BookmarkFolder(id: #file, title: #function) + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.selectedFolder = location + XCTAssertFalse(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: #file)) + } + + func testShouldAskBookmarkStoreToMoveFolderToRootFolderWhenSelectedFolderIsDifferentFromOriginalFolder() { + // GIVEN + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testShouldNotAskBookmarkStoreToMoveFolderWhenSelectedFolderIsNotDifferentFromOriginalFolder() { + // GIVEN + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } + +} diff --git a/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift index 32d827186b..269980c361 100644 --- a/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift +++ b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift @@ -91,6 +91,199 @@ class BookmarksBarViewModelTests: XCTestCase { XCTAssert(bookmarksBarViewModel.clippedItems.isEmpty) } + // MARK: - Bookmarks Delegate + + func testWhenItemFiresClickedActionThenDelegateReceivesClickItemActionAndPreventClickIsFalse() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemClicked(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .clickItem) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + + } + + func testWhenItemFiresOpenInNewTabActionThenDelegateReceivesOpenInNewTabAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemOpenInNewTabAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .openInNewTab) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresOpenInNewWindowActionThenDelegateReceivesOpenInNewWindowAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemOpenInNewWindowAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .openInNewWindow) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresToggleFavoritesActionThenDelegateReceivesToggleFavoritesAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemToggleFavoritesAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .toggleFavorites) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresEditActionThenDelegateReceivesEditAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewEditAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .edit) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresMoveToEndActionThenDelegateReceivesMoveToEndAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemMoveToEndAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .moveToEnd) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresCopyBookmarkURLActionThenDelegateReceivesCopyBookmarkURLAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemCopyBookmarkURLAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .copyURL) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresDeleteEntityActionThenDelegateReceivesDeleteEntityAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemDeleteEntityAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .deleteEntity) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresAddEntityActionThenDelegateReceivesAddEntityAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemAddEntityAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .addFolder) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresManageBookmarksActionThenDelegateReceivesManageBookmarksAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemManageBookmarksAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .manageBookmarks) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + private func createMockBookmarksManager(mockBookmarkStore: BookmarkStoreMock = BookmarkStoreMock()) -> BookmarkManager { let mockFaviconManager = FaviconManagerMock() return LocalBookmarkManager(bookmarkStore: mockBookmarkStore, faviconManagement: mockFaviconManager) @@ -107,3 +300,24 @@ fileprivate extension TabCollectionViewModel { } } + +// MARK: - BookmarksBarViewModelDelegateMock + +final class BookmarksBarViewModelDelegateMock: BookmarksBarViewModelDelegate { + private(set) var didCallViewModelReceivedAction = false + private(set) var capturedAction: BookmarksBarViewModel.BookmarksBarItemAction? + private(set) var capturedItem: BookmarksBarCollectionViewItem? + + func bookmarksBarViewModelReceived(action: BookmarksBarViewModel.BookmarksBarItemAction, for item: BookmarksBarCollectionViewItem) { + didCallViewModelReceivedAction = true + capturedAction = action + capturedItem = item + } + + func bookmarksBarViewModelWidthForContainer() -> CGFloat { + 0 + } + + func bookmarksBarViewModelReloadedData() {} + +} diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index c6a0840629..28f65fdf59 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -59,8 +59,12 @@ class MockBookmarkManager: BookmarkManager { func update(bookmark: DuckDuckGo_Privacy_Browser.Bookmark) {} + func update(bookmark: DuckDuckGo_Privacy_Browser.Bookmark, withURL url: URL, title: String, isFavorite: Bool) {} + func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder) {} + func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder, andMoveToParent parent: DuckDuckGo_Privacy_Browser.ParentFolderType) {} + func updateUrl(of bookmark: DuckDuckGo_Privacy_Browser.Bookmark, to newUrl: URL) -> DuckDuckGo_Privacy_Browser.Bookmark? { return nil } From 36dbe605f4291da51fbe5e1b0214b54c548fc1e0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 19 Mar 2024 11:46:32 +0100 Subject: [PATCH 10/17] Subscription design review further fixes (#2448) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206874000818381/f **Description**: Minor updates after the design review. **Steps to test this PR**: Please see comments in the description of https://app.asana.com/0/1200019156869587/1206659068942464/f --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Images/Privacy.imageset/Contents.json | 2 +- .../Images/Privacy.imageset/Privacy (Color).pdf | Bin 2879 -> 0 bytes .../Images/Privacy.imageset/Privacy.pdf | Bin 0 -> 4997 bytes .../Sources/SubscriptionUI/UserText.swift | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy (Color).pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json index 7b9656ec27..ec1dea3aba 100644 --- a/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Privacy (Color).pdf", + "filename" : "Privacy.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy (Color).pdf b/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy (Color).pdf deleted file mode 100644 index 5d44d31ebdea8ebefd11731ae29afc05d60054dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2879 zcmbVOO^@3)5WVwP@Dd;?5Slam27&<1Zc`L((XG>4(1Yt$HZFFp)^>_CzrJrMiIUer zzjQF@CvwDj@6C`;ZmwTHCzF&SZFT?4uS)9|FZ9cos@vbnU&Tv&^Y0{`!@^db9kuJ*wXo8CxH1r@`|!YVY#&&YH-jkki`-WlbndAz`I2 zmP*7R4};@&kJn@1DPGjFhHOmH=^3W?TpPp zoxWGWM2~1$d&9Qa;B+)$NG6yPJX4vGWU@tciPdHi8IKQ?j9@d!hEuSVHG(+e$eQp` zAMhzawxn#GK+aO2h`zm<$8D$XU#Hf0ZoU{6G#xJfLM%^bAdJj~!MnXrZI*rkPz(QuAF zdC5t1WN=RakWCIOSq@|9qO+YlBb!-eY9UHRHv0+OcXK>W9;PWBo1n( z&L}%8sEw{fUq?f+;VoX-JiHw-hKX|kZ-|LC1Da89CFdX^8yErl;1LmJ7Bk!dBLw0S z5D-G8VoQKuxS%UA;w2L;dPr>)#9Vs;^794Vpt>){7~nosallHj=|;pAc2Nw0#AV%9 zu?f;U;v8Q}7j^I9+qR8ns&TJ1=%bVtbqf;5 z3`0n@Kv3u4er^>=FGhj{XV5J7l<`vS34IXFCQiLIeuMCq;5#0tWP?sO=5*Y~WJ2GjvK9Zb=@;kWe+T zLO5!`#gabR_k~6xT{NY@&XPS@pIxZM`WPE#kuY9o$tfmt&>7KjYGbXZ6#czjbx#~Y)EnV#Y z2xJinEpHKNNb_oQ+T8bd<5Efb__eT+^)6VY9pM4hmP?)i>9mj^=Bd?!nC<3(ggAISW~SM0d$2ud;M4CfqRM6W zV+7n*+O0!(Iw~VFp8trfN3Wi}_)><_b<(?j`}&uz@4x!0Uwr@j`*pwg;k(zrtXBtU zJd1DEo41E|y=UXaM0&Z~et3VzV86$A#xpWrB5$|c?B6eU>&@!-Tkjt~oJFVBCi_T8_Y9NA~lU(T!fT|I3|bK|%>EO3SX)e!trA`mUrql$x|>#62hmXBB_;iJdgTntqH|?A}Wb;pQU-HHQGvaB>%`N z*-BH1xy-~k?hBFnn)iKzHG&Zcwtc~uL{+yM#B&7rL7vy3h3>OgTN4Q9YSPlA)Fpbn zK^?L!wMg|NAhUoJRHv$_1in$;kffI66idLPn0;j*tD<#H^-IQ~C=AI*${!qafT#f0s?VRREQcHLqQLQ`vI9G z2QO<8x&zyz#GKUOHP;#oM@Q!aS}WQLOPqz$H99cv8d?ToNrzFzR8b~GRd%98Rj;;^ ztph2asPb(PdQ4Q)RDNy90@=magkqB#<`w5!Ap<8}&GEjd4~`}&nv(+>Jp5>uN0ij$ z2~sWF6CuG&c9FamDDz_@QH-~>=oQcLjF0pd-ANbm9tVaY)LfF$LhwQ)q2(qOi{J+E zeWhT=NXI7Zj2Kk|xiKb0O=bxxm2eO=%^W!3rqe`XW-=Ro))u1ug=0pZOp}C1$laWZ znJ>kMQFE|9IMK=@w@Y*6tPlt~bGBsEmQY>ah;t$_tyCmbN>C#}{nR);uEN z)o6T2W^l*@h_a$%(Vw+bnL79`1rN~BjkWVy@zG`@p)WX(Ms6e=-&|9Y)}`rOC9_7E zJTPvEM}3?l)QQdlq!q@iUlX02VE|W4VpLEPF3^=Wa+)`xx9DYD>9lTfrP`qZbxo)x zW`O3RMagC62#fqsfJ;CUd5#=Vj2svMGDCxsJhKOfQKA)1R2t=5kW1k3a+xNNl0YjC zQ&26cogY$3#L`GY(kO|XBx_-%FH>49aW@&L`NFj0D;1xqoy@6ZJ3~@#R+-|eS8YkU zOe$72PrZ2T*u9 z4sFa^UdtCSa0Cp$5^TIq>fmR8-E;5;Tuj2a>N73FPuxr@Hu#?Gi)R|b$qCVZX z?Lz$nWuuWk-Fu&ZJiNR-EDvjY|6J_f<+scIzil;M+4bkG3-;+AetWfe*=x#8H~HJ| z?)_SDh5!2c{k!F#i#}e{@5xZY^j%@LAV#2*cV$7dOhu!k(dcEt^z5jTI zb3fZ}Z`PmppIyEAe01^RV!Oq^>OPCt;_~W`wfX|*?hnh|!Mqc5%iW_#&%b-|A76w6 ABme*a literal 0 HcmV?d00001 diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index f90aba85ad..1bd2c1503d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -103,9 +103,9 @@ enum UserText { static func activateModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { switch platform { case .appStore: - NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your subscription on other devices via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") + NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") case .stripe: - NSLocalizedString("subscription.activate.modal.description", value: "Access your subscription on other devices via an email address.", comment: "Activate subscription modal view subtitle description") + NSLocalizedString("subscription.activate.modal.description", value: "Access your Privacy Pro subscription via an email address.", comment: "Activate subscription modal view subtitle description") } } From 1a9bfbe410048d8775f117770839c922fb6d5a5f Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Tue, 19 Mar 2024 11:02:59 +0000 Subject: [PATCH 11/17] Web pixels (handlers + pixels) (#2451) Task/Issue URL: https://app.asana.com/0/72649045549333/1205469290776415/f Implementation sub-task: https://app.asana.com/0/72649045549333/1206697233126329/f **Description**: The FE needs to fire pixels after some user actions, this PR implements the web handlers where the related pixels are fired. --- DuckDuckGo/InfoPlist.xcstrings | 2 +- DuckDuckGo/Statistics/PixelEvent.swift | 10 +++ DuckDuckGo/Statistics/PixelParameters.swift | 6 +- .../SubscriptionPagesUserScript.swift | 67 ++++++++++++++++--- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index ee95494024..cce98e8330 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -363,4 +363,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 54aed95aef..0af3664f30 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -250,6 +250,11 @@ extension Pixel { case privacyProSubscriptionManagementEmail case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval + // Web pixels + case privacyProOfferMonthlyPriceClick + case privacyProOfferYearlyPriceClick + case privacyProAddEmailSuccess + case privacyProWelcomeFAQClick case dailyPixel(Event, isFirst: Bool) @@ -667,6 +672,11 @@ extension Pixel.Event { case .privacyProSubscriptionManagementEmail: return "m_mac_\(appDistribution)_privacy-pro_manage-email_edit_click" case .privacyProSubscriptionManagementPlanBilling: return "m_mac_\(appDistribution)_privacy-pro_settings_change-plan-or-billing_click" case .privacyProSubscriptionManagementRemoval: return "m_mac_\(appDistribution)_privacy-pro_settings_remove-from-device_click" + // Web + case .privacyProOfferMonthlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_monthly-price_click" + case .privacyProOfferYearlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_yearly-price_click" + case .privacyProAddEmailSuccess: return "m_mac_\(appDistribution)_privacy-pro_app_add-email_success_u" + case .privacyProWelcomeFAQClick: return "m_mac_\(appDistribution)_privacy-pro_welcome_faq_click_u" case .protectionToggledOffBreakageReport: return "m_mac_protection-toggled-off-breakage-report" case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 2313656cc7..c8be054048 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -201,7 +201,11 @@ extension Pixel.Event { .protectionToggledOffBreakageReport, .toggleProtectionsDailyCount, .toggleReportDoNotSend, - .toggleReportDismiss: + .toggleReportDismiss, + .privacyProOfferMonthlyPriceClick, + .privacyProOfferYearlyPriceClick, + .privacyProAddEmailSuccess, + .privacyProWelcomeFAQClick: return nil } } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index a69c43fa24..3c08e86e73 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -91,16 +91,40 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.broker = broker } + struct Handlers { + static let getSubscription = "getSubscription" + static let setSubscription = "setSubscription" + static let backToSettings = "backToSettings" + static let getSubscriptionOptions = "getSubscriptionOptions" + static let subscriptionSelected = "subscriptionSelected" + static let activateSubscription = "activateSubscription" + static let featureSelected = "featureSelected" + static let completeStripePayment = "completeStripePayment" + // Pixels related events + static let subscriptionsMonthlyPriceClicked = "subscriptionsMonthlyPriceClicked" + static let subscriptionsYearlyPriceClicked = "subscriptionsYearlyPriceClicked" + static let subscriptionsUnknownPriceClicked = "subscriptionsUnknownPriceClicked" + static let subscriptionsAddEmailSuccess = "subscriptionsAddEmailSuccess" + static let subscriptionsWelcomeFaqClicked = "subscriptionsWelcomeFaqClicked" + } + + // swiftlint:disable:next cyclomatic_complexity func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { switch methodName { - case "getSubscription": return getSubscription - case "setSubscription": return setSubscription - case "backToSettings": return backToSettings - case "getSubscriptionOptions": return getSubscriptionOptions - case "subscriptionSelected": return subscriptionSelected - case "activateSubscription": return activateSubscription - case "featureSelected": return featureSelected - case "completeStripePayment": return completeStripePayment + case Handlers.getSubscription: return getSubscription + case Handlers.setSubscription: return setSubscription + case Handlers.backToSettings: return backToSettings + case Handlers.getSubscriptionOptions: return getSubscriptionOptions + case Handlers.subscriptionSelected: return subscriptionSelected + case Handlers.activateSubscription: return activateSubscription + case Handlers.featureSelected: return featureSelected + case Handlers.completeStripePayment: return completeStripePayment + // Pixel related events + case Handlers.subscriptionsMonthlyPriceClicked: return subscriptionsMonthlyPriceClicked + case Handlers.subscriptionsYearlyPriceClicked: return subscriptionsYearlyPriceClicked + case Handlers.subscriptionsUnknownPriceClicked: return subscriptionsUnknownPriceClicked + case Handlers.subscriptionsAddEmailSuccess: return subscriptionsAddEmailSuccess + case Handlers.subscriptionsWelcomeFaqClicked: return subscriptionsWelcomeFaqClicked default: return nil } @@ -488,6 +512,33 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return [String: String]() // cannot be nil } + // MARK: Pixel related actions + + func subscriptionsMonthlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProOfferMonthlyPriceClick) + return nil + } + + func subscriptionsYearlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProOfferYearlyPriceClick) + return nil + } + + func subscriptionsUnknownPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + // Not used + return nil + } + + func subscriptionsAddEmailSuccess(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProAddEmailSuccess, limitTo: .initial) + return nil + } + + func subscriptionsWelcomeFaqClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProWelcomeFAQClick, limitTo: .initial) + return nil + } + // MARK: Push actions enum SubscribeActionName: String { From 3dc0733d527af2d2ed942f83564834148405eaa0 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 19 Mar 2024 04:21:57 -0700 Subject: [PATCH 12/17] Add VPN & PIR thank you modal (#2437) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206800069675138/f Tech Design URL: CC: **Description**: This PR adds a new modal to show a Thank You message to users. It isn't yet triggered within the app automatically, but has Debug menu triggers to show how it can be displayed. --- DuckDuckGo.xcodeproj/project.pbxproj | 16 ++ .../Gift-96.imageset/Contents.json | 5 +- .../Gift-96.imageset/Gift-96.pdf | Bin 9684 -> 0 bytes .../Gift-96.imageset/Gift-New-96x96.pdf | Bin 0 -> 7383 bytes .../UserText+NetworkProtection.swift | 14 ++ .../Model/HomePageContinueSetUpModel.swift | 45 +++- .../MainWindow/MainViewController.swift | 11 + DuckDuckGo/Menus/MainMenu.swift | 3 + DuckDuckGo/Menus/MainMenuActions.swift | 21 ++ .../WaitlistThankYouPromptPresenter.swift | 128 ++++++++++ .../Waitlist/Views/WaitlistThankYouView.swift | 229 ++++++++++++++++++ 11 files changed, 470 insertions(+), 2 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-96.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-New-96x96.pdf create mode 100644 DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift create mode 100644 DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 71f46f9bd0..ccafd9b0dd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1201,6 +1201,9 @@ 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; + 4B520F632BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */; }; + 4B520F642BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */; }; + 4B520F652BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */; }; 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023926B35F3600489384 /* ChromiumLoginReader.swift */; }; 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59024726B3673600489384 /* ThirdPartyBrowser.swift */; }; @@ -1220,6 +1223,9 @@ 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B68DDFF2ACBA14100FB0973 /* FileLineError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B696AFFA2AC5924800C93203 /* FileLineError.swift */; }; + 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; + 4B6B64852BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; + 4B6B64862BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */ = {isa = PBXBuildFile; fileRef = 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */; }; 4B70C00227B0793D000386ED /* CrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B70C00027B0793D000386ED /* CrashReportTests.swift */; }; 4B723E0526B0003E00E14D75 /* DataImportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DFF26B0003E00E14D75 /* DataImportMocks.swift */; }; @@ -3680,6 +3686,7 @@ 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; + 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistThankYouView.swift; sourceTree = ""; }; 4B59023926B35F3600489384 /* ChromiumLoginReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChromiumLoginReader.swift; sourceTree = ""; }; 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChromiumDataImporter.swift; sourceTree = ""; }; 4B59024726B3673600489384 /* ThirdPartyBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyBrowser.swift; sourceTree = ""; }; @@ -3698,6 +3705,7 @@ 4B677454255DC18000025BD8 /* Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Bridging.h; sourceTree = ""; }; 4B67853E2AA7C726008A5004 /* DailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyPixel.swift; sourceTree = ""; }; 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureDisabler.swift; sourceTree = ""; }; + 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistThankYouPromptPresenter.swift; sourceTree = ""; }; 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "DuckDuckGo-ExampleCrash.ips"; sourceTree = ""; }; 4B70C00027B0793D000386ED /* CrashReportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportTests.swift; sourceTree = ""; }; 4B723DEB26B0002B00E14D75 /* DataImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImport.swift; sourceTree = ""; }; @@ -5977,6 +5985,8 @@ 4B9DB0192A983B24000927DB /* WaitlistModalViewController.swift */, 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */, 4B9DB01A2A983B24000927DB /* WaitlistRootView.swift */, + 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */, + 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */, ); path = Views; sourceTree = ""; @@ -9900,6 +9910,7 @@ 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, + 4B520F642BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */, @@ -10426,6 +10437,7 @@ 4B9DB0452A983B24000927DB /* WaitlistModalViewController.swift in Sources */, B66CA41F2AD910B300447CF0 /* DataImportView.swift in Sources */, 3706FC0C293F65D500E42796 /* NSAttributedStringExtension.swift in Sources */, + 4B6B64852BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, 3706FC0D293F65D500E42796 /* AnimationView.swift in Sources */, 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, @@ -11476,6 +11488,7 @@ 4B957A9D2AC7AE700062CA31 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, 4B957A9E2AC7AE700062CA31 /* PrivacyDashboardPermissionHandler.swift in Sources */, 4B957A9F2AC7AE700062CA31 /* TabCollectionViewModel.swift in Sources */, + 4B520F652BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 4B957AA02AC7AE700062CA31 /* BookmarkManager.swift in Sources */, 4B957AA12AC7AE700062CA31 /* AboutModel.swift in Sources */, 4B957AA22AC7AE700062CA31 /* PasswordManagementCreditCardItemView.swift in Sources */, @@ -11732,6 +11745,7 @@ 4B957B802AC7AE700062CA31 /* NSAlertExtension.swift in Sources */, 4B957B812AC7AE700062CA31 /* ThirdPartyBrowser.swift in Sources */, 4B957B822AC7AE700062CA31 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, + 4B6B64862BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, B6B71C5A2B23379600487131 /* NSLayoutConstraintExtension.swift in Sources */, B65211272B29A43000B30633 /* BookmarkStoreMock.swift in Sources */, 4B957B832AC7AE700062CA31 /* CircularProgressView.swift in Sources */, @@ -12272,6 +12286,7 @@ 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, 1E7E2E942902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift in Sources */, AA9FF95F24A1FB690039E328 /* TabCollectionViewModel.swift in Sources */, + 4B520F632BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 4BF0E5052AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, AAC5E4D125D6A709007F5990 /* BookmarkManager.swift in Sources */, 37CD54CD27F2FDD100F1F7B9 /* AboutModel.swift in Sources */, @@ -12531,6 +12546,7 @@ 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, + 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json index 0223097db0..0d830310d5 100644 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Gift-96.pdf", + "filename" : "Gift-New-96x96.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-96.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-96.pdf deleted file mode 100644 index 41180d6d4063373a4e87e2a6ae4d3acaefac519f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9684 zcmeI2%WfUH5r+5u6umJp6F{xzwMk%LAY*$5K@!BVvm0cg9G~$x&?S*Hfs?1tS0uYg z=?I9t$;!uo-Ts>_)}2*kzj*ulhcmNm(`4p#zWLkVr+NPFyZQXh=ldVu-(TDx>;FRe z=WsEvi(Tb6zkJEali&Fp|Ll?W*(2SvN1Er1eECwiKVE+jaVPsg?mrx_Ki>Z`JHU4` z>hAvbcyTqIzdru!)8Y8;$M5G?@5lcg?xufE=WopX@oqwoMK|kwww^xe%jB0W6dk%; zo}!r#SJS!#CqGpD_~GtwJ%uIM6q23Y5@oWPuO`2^&3Na*QkLI*31$Ld0?VEmTYK=^ z++GQZGJFLQ0yfQusggD8To>9bwA>PSncJV9& z4`JCLWWsD0Cmav!$vh*H4SZdKtuo$q?#+@!Fkz-iXqE^N<1Sc6o^|msYD5oBUzcEJ z$3(Cc(RE>T?Sc(RdnL5mqY6eCKwuLwL@>0h>oviI2j!~@hRrU)8uwK&RH+iSR|%x) zp%OsvqMN3#x)3%K-FTK>J&TKAU@*9|qk^>u6+&$73OI@%s+Ox@DKxcV3tdmTm&(DG^g z9Hw8UpQr2T&n1HG7euf{OErQ$3g_ur5u6sAZNivg@uGf-;8@rTMk36&2#$+&#F~uD zV${$cwiR{ADPso}$*M)+3NLVb=dky^Z#R#K_$FER|62eYY3&-%F>xqXL=D~yVB_6WM_qUzQv*nl*_h6q-{*2NLRM#K=o(6X*q1uHx#UsbRY!ODFVj9jJ? z)>lcAPSZmr5JVo;S6#^047;&(kVI4xtCciYjeaVIB4VFh?W0&&9IjCmB7&*fu!XKC zKGOU~`4S%yH^Z{Zy?UTZsD7$?I85r#s;^5hakH{fh+q&^2U5WXMC~(RO|a@_Nvtn| zp=Di9{o!E*yDx}f%@{Tn?|)VRpQeznTkzK{`0EzDlH4?)w6^&2E!ax{YniN>trye! zE;}uuNm2Zs6~AUNW!vt1Mya~j2--1Smpc5>D&DD+O=Q3F7r4Yzx7pu-iBlCTX| zOdn2PDFG?}TE(Hu=x}9Nhb!_hL564R*I>N5znJ{|{AkKhdP7}b^cxGMj_J$OS4v>e zFXiEGv_`2~oMm*lBBe~O!xhs9rKE@wkj+`#sPNvF(c#Ll4p(H*g( z(-J6TmdxBJwa5rMLlY>iqoDxsIT?dy6=V??&4|S^irTsfD#f16E7i8lTep%B8iofo zmZf2l8aLZ{1X;{GDbYLFHAkt9|3q2=RjZ|T5jg^@bWn-{6cdr7gJ?T?M=jLWpvFl% zH_A(gsH^B%;Qtd{de ziP+Tx%_mT3c66(1i?b)pXpM2H?j2;0Z6g9D1bZIXDG&`OV~z_Ea3)CmdQ1 zHg>DAA~`w1RsV$-rZNjz^O4c3_4WYtMEl5qXTg-gsNfCQLpwU9ksUu zrtCnok5tZ~*-;g-trLk>)eo-SIO^EQXZH|jKH*KHBR_Rcr)pJsqx!E?9*ZZtB(99j zwB&9Fuly&lx;4r(NTqwUfrF)IOb1KWDsI+C5 zL4>$Jn7ATWk)@9%3%6G7YBI`3w*=m+X!i}!T0O9>j;L%tiJ&tB+9T5ukcQyQLtO8v zNh-}gQhcJ)=@h#^^|d=j{UzP*N^?GUwDaC6c5Ga`qr*rPFIvIXlEzaRjbQ(NLv2g`P3aGb2V_ZVXV7?nY+0!DeTEH8>g+x7-nshhMZYE zz^AO&GP|Yh$+Z$DH7R&GfNc|7sRQ~* zk~jMZ*_Z&U59EkaCnhfm8cNq(i!@RFeBRT&)q=A3Dpr zYKNql>`vQpLar28B7R3vct>^l2dMH%t+{ieGWM0ai1;X$eXJl{q^YV)K47{wv-?Pu zy;JPG_q97#O6lLU<5=}nb-Eq_`Yve7(W1Im8h%^xG(2g~&p86cn$vVgQPSv_!zPLm zssRPrU7JgC{gxJn)e&sp>o$V?(eJK0j;*v?8R)RBs8f=QBE2Q<2!#pZ|1&IUW5=D$*KlM(I z4kNb=E>e)m?O1kfMsmE6tY?$w#RIH>!OO zR%rmF$mU6o5uL@Kfs_eey1c>VfU#9jkZ>D8S2m2~Ks~y(oNETVS-gsuIY%yRVBBUG zsNR{&4M|vOipmw6UpW#c8&S;48YFHCUk}4bvc__E6kMPv7KT~VbLG|yCiy7_TCliZ zDwe}_yHY*Zi3}DEkqjN$f}bT*VN0*V)s&%65*DcclQhgL-H;QS$|=Q=;nf*>)Fs>oknkg@Fbb2m zl}g-mKd?h2fSi(~Co)Y3F$umQ7hjDU2@M62DEg`jiLrP76clT7K|=h_XA0u3Tio!p zvu$ZhjF7U@L1np9Qx)`875YB~7>*V_sdPRg4Q5*MaxuxRuM3_eCFsUIJqJ~Cl8`(J zrC@7bu>;a28tDg^>`h!Ul7h#PC1gm1569AxIzwqVt|RTY^*Hdrkp!;|+;U@jGVaDT z6USlY69q0+$^K`gYmRr}r9^^aF=J`@LPBSgd^{zZB!p6ule?LXFW^`td%`CKi^Dv? zXh;m1S1_GDXpX|ilUU?RPXdL#g!8O3c7c@Ho$3c69h82t+75QyZNWT*6(x9|MKsD9Ov`57axxe_~Y@zr;G1zKF>d4 zy;jHQL)H_LO>2#v(!1l`&9AqIZAFo+aKBU>fw->iyGyYMa;d2{- z`e#PJ|MUS?XOZCDysz`>=2zyL6aMc&21?}5fszP#rwj67QWN~u#r?(Q&BupRKi?jI zp4NGrs`&EvjGK@}f!j|$eURULy+c%ikDyEefd7=;`Y9XlYO+)ix+SH H^uvDv9PwaX diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-New-96x96.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-New-96x96.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f4fd0b70d68b50e25d46f98792d6a7276bc3c733 GIT binary patch literal 7383 zcmeHM%Z_8W5#8UfxSIj&0CUCn127E4GjR|kK_>0&j4V1DdE9}zm9*M{ldsPyiuV*- z5;8sTCX3Oc!9FC5^=1|Ct9Ng|eQlRKO?Ec(;ctJR=J~6y=C8k=b`S66-^q>m<`27v z4^KbN9^j3vIzBz_Ztthf+ugtJw!53(elx%MZv1b1oc=l4#kAjS*1>CzIOpm*={UK) zpUmQFF}`ANb~RYAvp63yoR8awDLC$m>*#*+W~niFlV(TMaivX*2{D0!VqCVJu7LXi zbU41m9A{gXoNRE=#d@;M>xj^HDi_#(@=Gi>I}xUJ7T20PdZXLk|R zcA!b?DY~D2oPL-dra!;f#i+eA7YhY0C3Y;nI-4*EE{ABFi$#j2N5jSH$;HaRy4plt zOtHbmu7L0fJ!f?>S1w!(%IoT(F#qQ+rUrfHV)@d=RMYBW14jL9Cq>mJqdOp_UBngj z#ktH;pjKPT@kJa0ZjejxHeldg4Aq?ZLs$=yG}X@|HiRW7_XFcs!C|-h_REARBp5mR zc>atO+)^3$6WLI3k1XD!RzV(ATO7A6-Sxm|A95DS4!TjQD|r&-aw0H1`6URYmecVW zhU&*Bj(C7VB)pvzB*Xgwq+EK0O0T>qwjFZP%j}jZ(V5-;cT(amJ^QoWX!KuXHwFU9 z>E+D1!0R*-JB3@TPT+}Y9oS@$M^3q8^-o>~_oxqIbK030VhuNtceQ2(EpNcI#FUNk zvqGe?ofK3e!-!P4MK>TEPNc&?np`OnLZpGfypT-tF&AiMgsXEYCdOiB1*vK47h6Mc zMnjo2hD^fsdp%;nu29jlt{gucq3e{$Fk+VA7TwTd|5v>rxfcAN-N?Y`m#$4+es&`$ z=$RWax6r$4ugstQI-cI$T9+wNzXFYI_!~n92%?jWMb0wb#74C_tBunk;P9` zS!DJlE2lcv%MYnEtUYv<&iT~WNRQHfnriPS7tjNx7^V2_vz}K*UTpFq#91LF0+G2; zspv>&b&q1(=?ZbOpi~Gck>Oe(4GylXCp6Y-FY?S%p#WoJY6?I{5Z&DLTvk1v59Eo$ zFUdrMb|Ng*xm@imS75c)nW9)&oXtK5AjP=sWH~y}Xfg&dl~_G814!7KGdkzRq5^P@ zzzOk-_qo)h``l7eO&&Z>uuKHwa;t`f2ZA)ix8)6@6zO(_eTxU6rQq^S>w`u^e6+R} z-M5{PfUM0X$;Jv_31(KvE~%E9iOsu`KsHoUG2M_m| zUn?rP%qQy`*3VIFU`RnSuJot~J($oh4zdXDK5iAb8&Z=f)`leo?~8|73>+s%Um6-r zbu9v_@wthNm@C!7fGwpO#xt~qjdLc78oAak#o@&ujOQTaaO)rZCyU3Gq2V zFu6UEL2t!{FgaZPL`PBWn)H-sSZC*1L*^tS;3wIamnB9kbK|J9iP^RVd~ye9Qd~tr zlkzG$aC}XQE^mMv=}M{wB&h~fAy2uYeL_v{&N;y*b{CeQ@*1gTiW*mYjCw?G-G0y1 z*gh0A6H`v!^lDZ@((88ih)K%s+z}(2q(2~?4WZS|MSg`AvF<&TZtzV;1oAj?#DF?@oRCuD*k5`*tKRg_sj`R0_#m6lCHh=l|KX&ux-R*~6 z2mZKwe|P)M;ph2#q_3`c4l2@M%BYNHjQZqC>1KC4{Peip9cTGmcGJndIXpb=9&n<3 zD_g;Lw~xSP6w(bEzNiVrFKwIO-@ON`vq*3=ujjlu{Di&sg8w6sff7ovfszP#obc&!^SifD#}~)L{qD>8i@Wz Bool + private let isPIRBetaTester: () -> Bool + private let userDefaults: UserDefaults + + convenience init() { + self.init(isVPNBetaTester: { + return false + }, isPIRBetaTester: { + return false + }) + } + + init(isVPNBetaTester: @escaping () -> Bool, isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { + self.isVPNBetaTester = isVPNBetaTester + self.isPIRBetaTester = isPIRBetaTester + self.userDefaults = userDefaults + } + + // MARK: - Presentation + + // Presents a Thank You prompt to testers of the VPN or PIR. + // If the user tested both, the PIR prompt will be displayed. + @MainActor + func presentThankYouPromptIfNecessary(in window: NSWindow) { + guard canShowPromptCheck() else { + return + } + + if isPIRBetaTester() { + saveDidShowPromptCheck() + presentPIRThankYouPrompt(in: window) + } else if isVPNBetaTester() { + saveDidShowPromptCheck() + presentVPNThankYouPrompt(in: window) + } + } + + @MainActor + func presentVPNThankYouPrompt(in window: NSWindow) { + let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .vpn) + let thankYouWindowController = thankYouModalView.wrappedInWindowController() + if let thankYouWindow = thankYouWindowController.window { + window.beginSheet(thankYouWindow) + } + } + + @MainActor + func presentPIRThankYouPrompt(in window: NSWindow) { + let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .dbp) + let thankYouWindowController = thankYouModalView.wrappedInWindowController() + if let thankYouWindow = thankYouWindowController.window { + window.beginSheet(thankYouWindow) + } + } + + // MARK: - Eligibility + + var canShowVPNCard: Bool { + guard !self.userDefaults.bool(forKey: Constants.didDismissVPNCardKey) else { + return false + } + + return isVPNBetaTester() + } + + var canShowPIRCard: Bool { + guard !self.userDefaults.bool(forKey: Constants.didDismissPIRCardKey) else { + return false + } + + return isPIRBetaTester() + } + + func canShowPromptCheck() -> Bool { + return !self.userDefaults.bool(forKey: Constants.didShowThankYouPromptKey) + } + + // MARK: - Dismissal + + func didDismissVPNThankYouCard() { + self.userDefaults.setValue(true, forKey: Constants.didDismissVPNCardKey) + } + + func didDismissPIRThankYouCard() { + self.userDefaults.setValue(true, forKey: Constants.didDismissPIRCardKey) + } + + private func saveDidShowPromptCheck() { + self.userDefaults.setValue(true, forKey: Constants.didShowThankYouPromptKey) + } + + // MARK: - Debug + + func resetPromptCheck() { + self.userDefaults.removeObject(forKey: Constants.didShowThankYouPromptKey) + self.userDefaults.removeObject(forKey: Constants.didDismissVPNCardKey) + self.userDefaults.removeObject(forKey: Constants.didDismissPIRCardKey) + } + +} diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift new file mode 100644 index 0000000000..b94d1846d2 --- /dev/null +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift @@ -0,0 +1,229 @@ +// +// WaitlistThankYouView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import SwiftUI + +// MARK: - Model + +struct WaitlistBetaThankYouCopy { + static let dbp = WaitlistBetaThankYouCopy( + title: UserText.dbpThankYouTitle, + subtitle: UserText.dbpThankYouSubtitle, + body1: UserText.dbpThankYouBody1, + body2: UserText.dbpThankYouBody2 + ) + + static let vpn = WaitlistBetaThankYouCopy( + title: UserText.vpnThankYouTitle, + subtitle: UserText.vpnThankYouSubtitle, + body1: UserText.vpnThankYouBody1, + body2: UserText.vpnThankYouBody2 + ) + + let title: String + let subtitle: String + let body1: String + let body2: String + + @available(macOS 12.0, *) + func boldedBold1() -> AttributedString { + return bolded(text: body1, boldedStrings: ["THANKYOU"]) + } + + @available(macOS 12.0, *) + func boldedBold2() -> AttributedString { + return bolded(text: body2, boldedStrings: ["duckduckgo.com/app"]) + } + + @available(macOS 12.0, *) + private func bolded(text: String, boldedStrings: [String]) -> AttributedString { + var attributedString = AttributedString(text) + + for boldedString in boldedStrings { + if let range = attributedString.range(of: boldedString) { + attributedString[range].font = .system(size: 14, weight: .semibold) + } + } + + return attributedString + } +} + +// MARK: - View Model + +protocol WaitlistBetaThankYouDialogViewModelDelegate: AnyObject { + func waitlistBetaThankYouViewModelDismissedView(_ viewModel: WaitlistBetaThankYouDialogViewModel) +} + +final class WaitlistBetaThankYouDialogViewModel: ObservableObject { + + enum ViewAction { + case close + } + + weak var delegate: WaitlistBetaThankYouDialogViewModelDelegate? + + init() {} + + @MainActor + func process(action: ViewAction) async { + switch action { + case .close: + delegate?.waitlistBetaThankYouViewModelDismissedView(self) + } + } + +} + +// MARK: - View + +final class WaitlistBetaThankYouDialogViewController: NSViewController { + + private let defaultSize = CGSize(width: 360, height: 498) + private let viewModel: WaitlistBetaThankYouDialogViewModel + + private var heightConstraint: NSLayoutConstraint? + private var cancellables = Set() + + private let copy: WaitlistBetaThankYouCopy + + init(copy: WaitlistBetaThankYouCopy) { + self.viewModel = WaitlistBetaThankYouDialogViewModel() + self.copy = copy + super.init(nibName: nil, bundle: nil) + self.viewModel.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NSView(frame: NSRect(origin: CGPoint.zero, size: defaultSize)) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let feedbackFormView = WaitlistBetaThankYouView(copy: self.copy) + let hostingView = NSHostingView(rootView: feedbackFormView.environmentObject(self.viewModel)) + hostingView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingView) + + let heightConstraint = hostingView.heightAnchor.constraint(equalToConstant: defaultSize.height) + self.heightConstraint = heightConstraint + + NSLayoutConstraint.activate([ + heightConstraint, + hostingView.widthAnchor.constraint(equalToConstant: defaultSize.width), + hostingView.topAnchor.constraint(equalTo: view.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostingView.leftAnchor.constraint(equalTo: view.leftAnchor), + hostingView.rightAnchor.constraint(equalTo: view.rightAnchor) + ]) + } + +} + +struct WaitlistBetaThankYouView: View { + + @EnvironmentObject var viewModel: WaitlistBetaThankYouDialogViewModel + + let copy: WaitlistBetaThankYouCopy + + var body: some View { + VStack(spacing: 0) { + VStack { + Text(copy.title) + .font(.system(size: 17, weight: .semibold)) + .padding([.leading, .trailing], 21.5) + .padding([.top, .bottom], 24) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity) + .background(Color.backgroundSecondary) + + Divider() + + Image("Gift-96") + .resizable() + .frame(width: 96, height: 96) + .padding([.top, .bottom], 24) + + Text(copy.subtitle) + .font(.system(size: 17, weight: .semibold)) + .padding([.leading, .trailing, .bottom], 14) + + if #available(macOS 12.0, *) { + Text(copy.boldedBold1()) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } else { + Text(copy.body1) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } + + if #available(macOS 12.0, *) { + Text(copy.boldedBold2()) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } else { + Text(copy.body2) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } + + Spacer() + + button(text: "Close", action: .close) + .padding(16) + } + .multilineTextAlignment(.center) + } + + @ViewBuilder + func button(text: String, action: WaitlistBetaThankYouDialogViewModel.ViewAction) -> some View { + Button(action: { + Task { + await viewModel.process(action: action) + } + }, label: { + Text(text) + .frame(maxWidth: .infinity) + }) + .controlSize(.large) + .keyboardShortcut(.defaultAction) + .frame(maxWidth: .infinity) + } + +} + +extension WaitlistBetaThankYouDialogViewController: WaitlistBetaThankYouDialogViewModelDelegate { + + func waitlistBetaThankYouViewModelDismissedView(_ viewModel: WaitlistBetaThankYouDialogViewModel) { + dismiss() + } + +} From 2f0b7f20c4883619e1afb9b4be4e3b2eb317bb04 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:28:04 +0100 Subject: [PATCH 13/17] Updated Settings Page (#2329) Task/Issue URL: https://app.asana.com/0/0/1206535251404777/f **Description**: Settings page reorganized to enhance discoverability of privacy protections and to unify structure across platforms --- DuckDuckGo.xcodeproj/project.pbxproj | 202 ++- .../Colors/AlertGreen.colorset/Contents.json | 20 + .../Contents.json | 2 +- .../Images/Accessibility.imageset/Icon 18.pdf | Bin 0 -> 6134 bytes .../Contents.json | 12 + .../CookieProtectionIcon.imageset/Icon 13.pdf | Bin 0 -> 3835 bytes .../Contents.json | 12 + .../EmailProtectionIcon.imageset/Icon 12.pdf | Bin 0 -> 6306 bytes .../FireSettings.imageset/Contents.json | 12 + .../Images/FireSettings.imageset/Icon 17.pdf | Bin 0 -> 33137 bytes .../Images/GeneralIcon.imageset/Contents.json | 12 + .../Images/GeneralIcon.imageset/Icon 16.pdf | Bin 0 -> 7715 bytes .../Images/HomePage/Contents.json | 6 + .../HomePage/Rocket.imageset/rocket.pdf | Bin 6315 -> 0 bytes .../Contents.json | 0 .../DownloadsPreferences.pdf | Bin .../PrivateSearchIcon.imageset/Contents.json | 12 + .../PrivateSearchIcon.imageset/Icon 10.pdf | Bin 0 -> 3482 bytes .../Contents.json | 12 + .../Icon 11.pdf | Bin 0 -> 4673 bytes .../Autoconsent/AutoconsentUserScript.swift | 6 +- .../ContentOverlayViewController.swift | 3 +- .../Common/Extensions/URLExtension.swift | 11 +- DuckDuckGo/Common/Localizables/UserText.swift | 45 +- .../ContentBlocker/ContentBlocking.swift | 2 +- .../ScriptSourceProviding.swift | 10 +- DuckDuckGo/DBP/DBPHomeViewController.swift | 4 +- DuckDuckGo/Email/EmailUrlExtensions.swift | 5 + .../Model/HomePageContinueSetUpModel.swift | 4 +- .../View/HomePageViewController.swift | 21 +- DuckDuckGo/InfoPlist.xcstrings | 16 +- DuckDuckGo/Localizable.xcstrings | 1155 ++++++++++++++++- .../View/AddressBarTextField.swift | 8 +- DuckDuckGo/Preferences/Model/AboutModel.swift | 7 +- .../Model/AccessibilityPreferences.swift | 69 + .../Model/AppearancePreferences.swift | 43 - .../CookiePopupProtectionPreferences.swift | 52 + ...el.swift => DataClearingPreferences.swift} | 47 +- .../Model/DefaultBrowserPreferences.swift | 28 + .../Model/PreferencesSection.swift | 110 +- .../Model/PreferencesSidebarModel.swift | 11 +- .../Model/PrivacyProtectionStatus.swift | 77 ++ .../Model/PrivacySecurityPreferences.swift | 41 - .../Preferences/Model/SearchPreferences.swift | 64 + .../WebTrackingProtectionPreferences.swift | 52 + .../View/PreferencesAboutView.swift | 4 +- .../View/PreferencesAccessibilityView.swift | 54 + .../View/PreferencesAppearanceView.swift | 19 - ...PreferencesCookiePopupProtectionView.swift | 55 + .../View/PreferencesDataClearingView.swift | 54 + .../View/PreferencesDefaultBrowserView.swift | 69 + .../View/PreferencesDownloadsView.swift | 65 - .../View/PreferencesEmailProtectionView.swift | 81 ++ .../View/PreferencesGeneralView.swift | 61 +- .../View/PreferencesPrivacyView.swift | 88 -- .../View/PreferencesPrivateSearchView.swift | 55 + .../View/PreferencesRootView.swift | 129 +- .../Preferences/View/PreferencesSidebar.swift | 99 +- .../View/PreferencesSyncView.swift | 47 + ...PreferencesWebTrackingProtectionView.swift | 61 + .../View/PrivacyDashboardViewController.swift | 4 +- .../View/SaveCredentialsViewController.swift | 2 +- DuckDuckGo/Statistics/PixelEvent.swift | 12 + DuckDuckGo/Statistics/PixelParameters.swift | 4 + .../SuggestionContainerViewModel.swift | 2 +- .../Tab/Model/UserContentUpdating.swift | 6 +- .../TabExtensions/AutofillTabExtension.swift | 2 +- .../NavigationProtectionTabExtension.swift | 3 +- DuckDuckGo/Tab/UserScripts/UserScripts.swift | 4 +- .../Tab/View/BrowserTabViewController.swift | 3 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 12 +- .../AutoconsentIntegrationTests.swift | 17 +- .../HTTPSUpgradeIntegrationTests.swift | 4 +- .../History/HistoryIntegrationTests.swift | 6 +- ...NavigationProtectionIntegrationTests.swift | 10 +- .../PrivacyDashboardIntegrationTests.swift | 4 +- .../Sources/PreferencesViews/Constants.swift | 1 + .../InfoPlist.xcstrings | 186 +++ .../AutoconsentMessageProtocolTests.swift | 4 +- .../ContentBlockingUpdatingTests.swift | 6 +- .../HomePage/ContinueSetUpModelTests.swift | 27 +- .../AccessibilityPreferencesTests.swift | 44 + .../AppearancePreferencesTests.swift | 26 - ...ookiePopupProtectionPreferencesTests.swift | 44 + .../DataClearingPreferencesTests.swift | 43 + .../PreferencesSidebarModelTests.swift | 6 +- .../PrivacyProtectionStatusTests.swift | 67 + .../Preferences/SearchPreferencesTests.swift | 44 + ...ebTrackingProtectionPreferencesTests.swift | 44 + .../Tab/ViewModel/TabViewModelTests.swift | 6 +- UnitTests/Tab/WebViewTests.swift | 4 +- ...wModelTests+WithoutPinnedTabsManager.swift | 4 +- .../TabCollectionViewModelTests.swift | 4 +- 93 files changed, 3115 insertions(+), 674 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json rename DuckDuckGo/Assets.xcassets/Images/{HomePage/Rocket.imageset => Accessibility.imageset}/Contents.json (78%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Icon 18.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/CookieProtectionIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/CookieProtectionIcon.imageset/Icon 13.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Icon 12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Icon 17.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Icon 16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/HomePage/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/rocket.pdf rename DuckDuckGo/Assets.xcassets/Images/{DownloadsPreferences.imageset => OtherPlatformsPreferences.imageset}/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Images/{DownloadsPreferences.imageset => OtherPlatformsPreferences.imageset}/DownloadsPreferences.pdf (100%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Icon 10.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Icon 11.pdf create mode 100644 DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift create mode 100644 DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift rename DuckDuckGo/Preferences/Model/{PrivacyPreferencesModel.swift => DataClearingPreferences.swift} (51%) create mode 100644 DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift delete mode 100644 DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift create mode 100644 DuckDuckGo/Preferences/Model/SearchPreferences.swift create mode 100644 DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift delete mode 100644 DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift delete mode 100644 DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesSyncView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift create mode 100644 NetworkProtectionAppExtension/InfoPlist.xcstrings create mode 100644 UnitTests/Preferences/AccessibilityPreferencesTests.swift create mode 100644 UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift create mode 100644 UnitTests/Preferences/DataClearingPreferencesTests.swift create mode 100644 UnitTests/Preferences/PrivacyProtectionStatusTests.swift create mode 100644 UnitTests/Preferences/SearchPreferencesTests.swift create mode 100644 UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ccafd9b0dd..66a3b706f0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -17,6 +17,15 @@ 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14505A07256084EF00272CC6 /* UserAgent.swift */; }; 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */; }; 14D9B8FB24F7E089000D4D13 /* AddressBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */; }; + 1D01A3D02B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */; }; + 1D01A3D12B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */; }; + 1D01A3D22B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */; }; + 1D01A3D42B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */; }; + 1D01A3D52B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */; }; + 1D01A3D62B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */; }; + 1D01A3D82B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; + 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; + 1D01A3DA2B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; }; 1D074B272909A433006E4AC3 /* PasswordManagerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */; }; 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; @@ -26,6 +35,12 @@ 1D1C36E429FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1C36E229FAE8DA001FA40C /* FaviconManagerTests.swift */; }; 1D1C36E629FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */; }; 1D1C36E729FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */; }; + 1D220BF82B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; + 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; + 1D220BFA2B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; + 1D220BFC2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; + 1D220BFD2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; + 1D220BFE2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; 1D26EBAC2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBAE2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; @@ -62,6 +77,7 @@ 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */; }; 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; 1D8057C92A83CB3C00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; + 1D85BCCA2BA982FC0065BA04 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1D85BCC92BA982FC0065BA04 /* InfoPlist.xcstrings */; }; 1D8B7D6A2A38BF050045C6F6 /* FireproofDomainsStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */; }; 1D8B7D6B2A38BF060045C6F6 /* FireproofDomainsStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */; }; 1D8C2FE52B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */; }; @@ -75,6 +91,18 @@ 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5C2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; + 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; + 1D9FDEB82B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; + 1D9FDEBA2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */; }; + 1D9FDEBB2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */; }; + 1D9FDEBD2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */; }; + 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */; }; + 1D9FDEC02B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */; }; + 1D9FDEC12B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */; }; + 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */; }; + 1D9FDEC42B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */; }; + 1D9FDEC62B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */; }; + 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */; }; 1DA6D0FD2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D0FE2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; @@ -94,6 +122,27 @@ 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC6696F2B6CF0D700AA0645 /* TabSnapshotStore.swift */; }; 1DCFBC8A29ADF32B00313531 /* BurnerHomePageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCFBC8929ADF32B00313531 /* BurnerHomePageView.swift */; }; 1DCFBC8B29ADF32B00313531 /* BurnerHomePageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCFBC8929ADF32B00313531 /* BurnerHomePageView.swift */; }; + 1DDC84F72B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */; }; + 1DDC84F82B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */; }; + 1DDC84F92B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */; }; + 1DDC84FB2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */; }; + 1DDC84FC2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */; }; + 1DDC84FD2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */; }; + 1DDC84FF2B835BC000670238 /* SearchPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */; }; + 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */; }; + 1DDC85012B835BC000670238 /* SearchPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */; }; + 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */; }; + 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */; }; + 1DDC85052B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */; }; + 1DDD3EBC2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */; }; + 1DDD3EBD2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */; }; + 1DDD3EBE2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */; }; + 1DDD3EC02B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */; }; + 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */; }; + 1DDD3EC22B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */; }; + 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */; }; + 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */; }; + 1DDD3EC62B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */; }; 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */; }; 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */; }; 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; @@ -318,7 +367,7 @@ 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92928626670D1600AD2C21 /* OutlineSeparatorViewCell.swift */; }; 3706FB1B293F65D500E42796 /* SafariDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CFD26FE191E001E4761 /* SafariDataImporter.swift */; }; 3706FB1D293F65D500E42796 /* StatisticsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50342726A11F00758A2B /* StatisticsLoader.swift */; }; - 3706FB1F293F65D500E42796 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; }; + 3706FB1F293F65D500E42796 /* DataClearingPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */; }; 3706FB20293F65D500E42796 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; 3706FB21293F65D500E42796 /* NavigationBarBadgeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */; }; 3706FB22293F65D500E42796 /* NSTextViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */; }; @@ -400,7 +449,6 @@ 3706FB83293F65D500E42796 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; 3706FB84293F65D500E42796 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9E9A5525A3AE8400D1959D /* NSWindowExtension.swift */; }; 3706FB85293F65D500E42796 /* AddBookmarkPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */; }; - 3706FB86293F65D500E42796 /* PreferencesDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */; }; 3706FB87293F65D500E42796 /* ProcessExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */; }; 3706FB88293F65D500E42796 /* PermissionAuthorizationQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA526A7BEC80013B453 /* PermissionAuthorizationQuery.swift */; }; 3706FB89293F65D500E42796 /* BadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B9288984D00068632A /* BadgeAnimationView.swift */; }; @@ -418,7 +466,6 @@ 3706FB95293F65D500E42796 /* PermissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BAA26A7BF1D0013B453 /* PermissionType.swift */; }; 3706FB96293F65D500E42796 /* RecentlyClosedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881A28626C1900D54247 /* RecentlyClosedWindow.swift */; }; 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F29276A35FE00DC0649 /* ActionSpeech.swift */; }; - 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */; }; 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */; }; 3706FB9C293F65D500E42796 /* TabCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */; }; @@ -509,7 +556,7 @@ 3706FC01293F65D500E42796 /* ChromiumBookmarksReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CF926FE191E001E4761 /* ChromiumBookmarksReader.swift */; }; 3706FC02293F65D500E42796 /* Downloads.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */; }; 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; - 3706FC04293F65D500E42796 /* PreferencesPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */; }; + 3706FC04293F65D500E42796 /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; 3706FC05293F65D500E42796 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; 3706FC06293F65D500E42796 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F30276A7DCA00DC0649 /* OnboardingViewModel.swift */; }; 3706FC07293F65D500E42796 /* ScriptSourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B0425D6B1D800C7D2AA /* ScriptSourceProviding.swift */; }; @@ -1022,15 +1069,14 @@ 37BF3F22286F0A7A00BD9014 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; - 37CC53EC27E8A4D10028713D /* PreferencesPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */; }; - 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */; }; + 37CC53EC27E8A4D10028713D /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; 37CC53F427E8D4620028713D /* NSPathControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53F327E8D4620028713D /* NSPathControlView.swift */; }; 37CD54B527F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54B427F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift */; }; 37CD54B727F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54B627F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift */; }; 37CD54B927F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */; }; 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */; }; 37CD54BD27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54BC27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift */; }; - 37CD54C927F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; }; + 37CD54C927F2FDD100F1F7B9 /* DataClearingPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */; }; 37CD54CA27F2FDD100F1F7B9 /* AutofillPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C227F2FDD100F1F7B9 /* AutofillPreferencesModel.swift */; }; 37CD54CB27F2FDD100F1F7B9 /* DownloadsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */; }; 37CD54CC27F2FDD100F1F7B9 /* PreferencesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C427F2FDD100F1F7B9 /* PreferencesSection.swift */; }; @@ -1060,7 +1106,6 @@ 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */; }; 4B02199C25E063DE00ED7DEA /* FireproofDomainsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02199925E063DE00ED7DEA /* FireproofDomainsTests.swift */; }; 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0219A725E0646500ED7DEA /* WebsiteDataStoreTests.swift */; }; - 4B0511BD262CAA5A00F6079C /* PrivacySecurityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */; }; 4B0511C3262CAA5A00F6079C /* FireproofDomains.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B0511AD262CAA5A00F6079C /* FireproofDomains.storyboard */; }; 4B0511CA262CAA5A00F6079C /* FireproofDomainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B4262CAA5A00F6079C /* FireproofDomainsViewController.swift */; }; 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; }; @@ -1466,7 +1511,7 @@ 4B957A032AC7AE700062CA31 /* LocalBookmarkStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 987799F829999973005D8EB6 /* LocalBookmarkStore.swift */; }; 4B957A042AC7AE700062CA31 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; }; 4B957A052AC7AE700062CA31 /* StatisticsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50342726A11F00758A2B /* StatisticsLoader.swift */; }; - 4B957A072AC7AE700062CA31 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; }; + 4B957A072AC7AE700062CA31 /* DataClearingPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */; }; 4B957A082AC7AE700062CA31 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; 4B957A092AC7AE700062CA31 /* InternalUserDeciderStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D36E657298AA3BA00AA485D /* InternalUserDeciderStore.swift */; }; 4B957A0A2AC7AE700062CA31 /* NewWindowPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B634DBE4293C944700C3C99E /* NewWindowPolicy.swift */; }; @@ -1584,7 +1629,6 @@ 4B957A812AC7AE700062CA31 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; 4B957A822AC7AE700062CA31 /* SyncDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370A34B02AB24E3700C77F7C /* SyncDebugMenu.swift */; }; 4B957A832AC7AE700062CA31 /* AddBookmarkPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */; }; - 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */; }; 4B957A852AC7AE700062CA31 /* QRSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingService.swift */; }; 4B957A862AC7AE700062CA31 /* ProcessExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */; }; 4B957A872AC7AE700062CA31 /* PermissionAuthorizationQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA526A7BEC80013B453 /* PermissionAuthorizationQuery.swift */; }; @@ -1605,7 +1649,6 @@ 4B957A962AC7AE700062CA31 /* PermissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BAA26A7BF1D0013B453 /* PermissionType.swift */; }; 4B957A982AC7AE700062CA31 /* RecentlyClosedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881A28626C1900D54247 /* RecentlyClosedWindow.swift */; }; 4B957A992AC7AE700062CA31 /* ActionSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F29276A35FE00DC0649 /* ActionSpeech.swift */; }; - 4B957A9A2AC7AE700062CA31 /* PrivacySecurityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */; }; 4B957A9B2AC7AE700062CA31 /* ModalSheetCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BE9FA9293F7955006363C6 /* ModalSheetCancellable.swift */; }; 4B957A9C2AC7AE700062CA31 /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; 4B957A9D2AC7AE700062CA31 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; @@ -1725,7 +1768,7 @@ 4B957B152AC7AE700062CA31 /* ChromiumBookmarksReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CF926FE191E001E4761 /* ChromiumBookmarksReader.swift */; }; 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */; }; 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; - 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */; }; + 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F30276A7DCA00DC0649 /* OnboardingViewModel.swift */; }; 4B957B1B2AC7AE700062CA31 /* ScriptSourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B0425D6B1D800C7D2AA /* ScriptSourceProviding.swift */; }; @@ -3397,6 +3440,9 @@ 14505A07256084EF00272CC6 /* UserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgent.swift; sourceTree = ""; }; 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarCollectionView.swift; sourceTree = ""; }; 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarViewController.swift; sourceTree = ""; }; + 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesAccessibilityView.swift; sourceTree = ""; }; + 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityPreferences.swift; sourceTree = ""; }; + 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesSyncView.swift; sourceTree = ""; }; 1D02633428D8A9A9005CBB41 /* BWEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BWEncryption.h; sourceTree = ""; }; 1D02633528D8A9A9005CBB41 /* BWEncryption.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BWEncryption.m; sourceTree = ""; }; 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagerCoordinator.swift; sourceTree = ""; }; @@ -3404,6 +3450,8 @@ 1D1A33482A6FEB170080ACED /* BurnerMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurnerMode.swift; sourceTree = ""; }; 1D1C36E229FAE8DA001FA40C /* FaviconManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconManagerTests.swift; sourceTree = ""; }; 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTabExtensionTests.swift; sourceTree = ""; }; + 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesEmailProtectionView.swift; sourceTree = ""; }; + 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatus.swift; sourceTree = ""; }; 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageSendable.swift; sourceTree = ""; }; 1D26EBAF2B74DB600002A93F /* TabSnapshotCleanupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotCleanupService.swift; sourceTree = ""; }; 1D36E657298AA3BA00AA485D /* InternalUserDeciderStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderStore.swift; sourceTree = ""; }; @@ -3428,11 +3476,18 @@ 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconStoringMock.swift; sourceTree = ""; }; 1D77921C28FFF27C00BE0210 /* RunningApplicationCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningApplicationCheck.swift; sourceTree = ""; }; 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedOsChecker.swift; sourceTree = ""; }; + 1D85BCC92BA982FC0065BA04 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtensionTests.swift; sourceTree = ""; }; 1D8C2FE92B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTabSnapshotStore.swift; sourceTree = ""; }; 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtension.swift; sourceTree = ""; }; + 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CookiePopupProtectionPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataClearingPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatusTests.swift; sourceTree = ""; }; 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookie.swift; sourceTree = ""; }; 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookieTests.swift; sourceTree = ""; }; 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSnapshotRenderer.swift; sourceTree = ""; }; @@ -3441,6 +3496,13 @@ 1DB9617F29F67F3E00CF5568 /* FaviconNullStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconNullStore.swift; sourceTree = ""; }; 1DC6696F2B6CF0D700AA0645 /* TabSnapshotStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotStore.swift; sourceTree = ""; }; 1DCFBC8929ADF32B00313531 /* BurnerHomePageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurnerHomePageView.swift; sourceTree = ""; }; + 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPrivateSearchView.swift; sourceTree = ""; }; + 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesDefaultBrowserView.swift; sourceTree = ""; }; + 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferences.swift; sourceTree = ""; }; + 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWebTrackingProtectionView.swift; sourceTree = ""; }; + 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferences.swift; sourceTree = ""; }; + 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesCookiePopupProtectionView.swift; sourceTree = ""; }; + 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CookiePopupProtectionPreferences.swift; sourceTree = ""; }; 1DDF075C28F815AD00EDFBE3 /* BWCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWCredential.swift; sourceTree = ""; }; 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWCommunicator.swift; sourceTree = ""; }; 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWManager.swift; sourceTree = ""; }; @@ -3565,15 +3627,14 @@ 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; - 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesPrivacyView.swift; sourceTree = ""; }; - 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDownloadsView.swift; sourceTree = ""; }; + 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; 37CC53F327E8D4620028713D /* NSPathControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPathControlView.swift; sourceTree = ""; }; 37CD54B427F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesSidebarModelTests.swift; sourceTree = ""; }; 37CD54B627F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBrowserPreferencesTests.swift; sourceTree = ""; }; 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferencesTests.swift; sourceTree = ""; }; 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsPreferencesTests.swift; sourceTree = ""; }; 37CD54BC27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesModelTests.swift; sourceTree = ""; }; - 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPreferencesModel.swift; sourceTree = ""; }; + 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataClearingPreferences.swift; sourceTree = ""; }; 37CD54C227F2FDD100F1F7B9 /* AutofillPreferencesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesModel.swift; sourceTree = ""; }; 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadsPreferences.swift; sourceTree = ""; }; 37CD54C427F2FDD100F1F7B9 /* PreferencesSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesSection.swift; sourceTree = ""; }; @@ -3602,7 +3663,6 @@ 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomains.swift; sourceTree = ""; }; 4B02199925E063DE00ED7DEA /* FireproofDomainsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomainsTests.swift; sourceTree = ""; }; 4B0219A725E0646500ED7DEA /* WebsiteDataStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebsiteDataStoreTests.swift; sourceTree = ""; }; - 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacySecurityPreferences.swift; sourceTree = ""; }; 4B0511AD262CAA5A00F6079C /* FireproofDomains.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = FireproofDomains.storyboard; sourceTree = ""; }; 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedSelectionRowView.swift; sourceTree = ""; }; 4B0511B4262CAA5A00F6079C /* FireproofDomainsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomainsViewController.swift; sourceTree = ""; }; @@ -5315,18 +5375,22 @@ children = ( 37CD54C427F2FDD100F1F7B9 /* PreferencesSection.swift */, 37CD54C627F2FDD100F1F7B9 /* PreferencesSidebarModel.swift */, - 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */, 37CD54C827F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift */, + 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */, + 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */, + 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */, + 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */, + 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */, + 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */, 3775912C29AAC72700E26367 /* SyncPreferences.swift */, 37CD54C727F2FDD100F1F7B9 /* AppearancePreferences.swift */, - 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */, - 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */, - 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */, 37CD54C227F2FDD100F1F7B9 /* AutofillPreferencesModel.swift */, 3776582E27F82E62009A6B35 /* AutofillPreferences.swift */, - 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */, + 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */, + 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */, + 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */, 37CD54C527F2FDD100F1F7B9 /* AboutModel.swift */, - 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */, + 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */, ); path = Model; sourceTree = ""; @@ -5404,13 +5468,19 @@ 37AFCE8627DA334800471A10 /* PreferencesRootView.swift */, 37AFCE8427DA2D3900471A10 /* PreferencesSidebar.swift */, 37AFCE8A27DB69BC00471A10 /* PreferencesGeneralView.swift */, + 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */, + 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */, + 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */, + 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */, + 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */, + 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */, + 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */, 37D2771427E870D4003365FD /* PreferencesAppearanceView.swift */, - 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */, - 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */, 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */, - 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */, + 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */, + 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */, + 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */, 37AFCE9127DB8CAD00471A10 /* PreferencesAboutView.swift */, - 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */, ); path = View; sourceTree = ""; @@ -5420,12 +5490,18 @@ children = ( 37CD54B427F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift */, 37CD54B627F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift */, - 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */, + 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */, + 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */, + 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */, + 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */, 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */, + 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */, 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */, 37CD54BC27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift */, - 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */, + 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */, + 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */, 3714B1E628EDB7FA0056C57A /* DuckPlayerPreferencesTests.swift */, + 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */, ); path = Preferences; sourceTree = ""; @@ -5711,6 +5787,7 @@ 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */ = { isa = PBXGroup; children = ( + 1D85BCC92BA982FC0065BA04 /* InfoPlist.xcstrings */, 4B5F14F82A148B230060320F /* Info.plist */, ); path = NetworkProtectionAppExtension; @@ -9432,6 +9509,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1D85BCCA2BA982FC0065BA04 /* InfoPlist.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9936,6 +10014,7 @@ 3706FA94293F65D500E42796 /* TabShadowConfig.swift in Sources */, 3706FA97293F65D500E42796 /* WindowDraggingView.swift in Sources */, B60D644A2AAF1B7C00B26F50 /* AddressBarTextSelectionNavigation.swift in Sources */, + 1D01A3D52B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, 3706FA98293F65D500E42796 /* SecureVaultSorting.swift in Sources */, 3706FA99293F65D500E42796 /* PreferencesSidebarModel.swift in Sources */, 3706FA9A293F65D500E42796 /* DuckPlayerURLExtension.swift in Sources */, @@ -9976,6 +10055,7 @@ 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, + 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9988,10 +10068,12 @@ F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */, 3706FAC4293F65D500E42796 /* PrintingUserScript.swift in Sources */, + 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, 9D9AE86C2AA76D1B0026E7DC /* LoginItemsManager.swift in Sources */, 4B9DB0392A983B24000927DB /* JoinedWaitlistView.swift in Sources */, 3706FEBF293F6EFF00E42796 /* BWError.swift in Sources */, 3706FAC6293F65D500E42796 /* ConnectBitwardenViewController.swift in Sources */, + 1DDC84FC2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */, 3706FAC8293F65D500E42796 /* AppTrackerDataSetProvider.swift in Sources */, 3706FAC9293F65D500E42796 /* EncryptionKeyGeneration.swift in Sources */, @@ -10018,6 +10100,8 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, + 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, + 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, @@ -10074,6 +10158,7 @@ 3706FB07293F65D500E42796 /* Publisher.asVoid.swift in Sources */, 3706FB08293F65D500E42796 /* NavigationButtonMenuDelegate.swift in Sources */, 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */, + 1DDC84F82B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, 3706FB09293F65D500E42796 /* CrashReport.swift in Sources */, 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */, 3706FB0A293F65D500E42796 /* NSPathControlView.swift in Sources */, @@ -10100,7 +10185,8 @@ 3793FDD829535EBA00A2E28F /* Assertions.swift in Sources */, B62B48572ADE730D000DECE5 /* FileImportView.swift in Sources */, B6676BE22AA986A700525A21 /* AddressBarTextEditor.swift in Sources */, - 3706FB1F293F65D500E42796 /* PrivacyPreferencesModel.swift in Sources */, + 3706FB1F293F65D500E42796 /* DataClearingPreferences.swift in Sources */, + 1D01A3D12B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 3706FB20293F65D500E42796 /* LocalUnprotectedDomains.swift in Sources */, 3707C719294B5D0F00682A9F /* HoveredLinkTabExtension.swift in Sources */, 3706FB21293F65D500E42796 /* NavigationBarBadgeAnimator.swift in Sources */, @@ -10237,7 +10323,6 @@ 37197EA42942441D00394917 /* NewWindowPolicy.swift in Sources */, 3706FB84293F65D500E42796 /* NSWindowExtension.swift in Sources */, 3706FB85293F65D500E42796 /* AddBookmarkPopover.swift in Sources */, - 3706FB86293F65D500E42796 /* PreferencesDownloadsView.swift in Sources */, 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */, 3706FB87293F65D500E42796 /* ProcessExtension.swift in Sources */, 3706FB88293F65D500E42796 /* PermissionAuthorizationQuery.swift in Sources */, @@ -10266,7 +10351,6 @@ B690152D2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */, 1D36F4252A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */, - 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */, B6AFE6BD29A5D621002FF962 /* HTTPSUpgradeTabExtension.swift in Sources */, 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */, 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */, @@ -10285,6 +10369,7 @@ 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, + 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, 3706FBA7293F65D500E42796 /* EncryptedValueTransformer.swift in Sources */, @@ -10337,6 +10422,7 @@ 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, + 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, B6D6A5DD2982A4CE001F5F11 /* Tab+Navigation.swift in Sources */, @@ -10397,6 +10483,7 @@ 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */, + 1D220BFD2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */, 3706FBF6293F65D500E42796 /* Pixel.swift in Sources */, 3706FBF7293F65D500E42796 /* PixelEvent.swift in Sources */, 3706FBF8293F65D500E42796 /* TabBarFooter.swift in Sources */, @@ -10419,7 +10506,7 @@ 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, 4B9754EC2984300100D7B834 /* EmailManagerExtension.swift in Sources */, - 3706FC04293F65D500E42796 /* PreferencesPrivacyView.swift in Sources */, + 3706FC04293F65D500E42796 /* PreferencesDataClearingView.swift in Sources */, 3706FC05293F65D500E42796 /* NSPasteboardExtension.swift in Sources */, 1DCFBC8B29ADF32B00313531 /* BurnerHomePageView.swift in Sources */, 3706FC06293F65D500E42796 /* OnboardingViewModel.swift in Sources */, @@ -10462,10 +10549,12 @@ 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, 3706FC1F293F65D500E42796 /* BookmarksBarViewController.swift in Sources */, + 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 3706FC20293F65D500E42796 /* PreferencesAutofillView.swift in Sources */, 3706FC21293F65D500E42796 /* UserText+PasswordManager.swift in Sources */, 3706FC22293F65D500E42796 /* LoadingProgressView.swift in Sources */, 3706FC23293F65D500E42796 /* StatisticsStore.swift in Sources */, + 1DDD3EBD2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, 3706FC25293F65D500E42796 /* ColorView.swift in Sources */, 3706FC26293F65D500E42796 /* RecentlyClosedCacheItem.swift in Sources */, 3706FC27293F65D500E42796 /* PopupBlockedPopover.swift in Sources */, @@ -10672,6 +10761,7 @@ 3706FDF0293F661700E42796 /* WebKitVersionProviderTests.swift in Sources */, 3706FDF1293F661700E42796 /* AtbAndVariantCleanupTests.swift in Sources */, 1D1C36E729FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */, + 1D9FDEBB2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */, 3706FDF3293F661700E42796 /* ChromiumLoginReaderTests.swift in Sources */, 3706FDF4293F661700E42796 /* TabCollectionTests.swift in Sources */, 3706FDF5293F661700E42796 /* StartupPreferencesTests.swift in Sources */, @@ -10695,6 +10785,7 @@ 3706FE01293F661700E42796 /* PixelStoreMock.swift in Sources */, 3706FE02293F661700E42796 /* BookmarksBarViewModelTests.swift in Sources */, 3706FE03293F661700E42796 /* CoreDataStoreTests.swift in Sources */, + 1D9FDEC42B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 3706FE04293F661700E42796 /* TreeControllerTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, @@ -10709,6 +10800,7 @@ 3706FE0E293F661700E42796 /* FirefoxDataImporterTests.swift in Sources */, 3706FE0F293F661700E42796 /* CSVLoginExporterTests.swift in Sources */, 3706FE10293F661700E42796 /* TestNavigationDelegate.swift in Sources */, + 1D9FDEC12B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, 3706FE11293F661700E42796 /* URLSuggestedFilenameTests.swift in Sources */, 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, @@ -10726,6 +10818,7 @@ 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, 3706FE1D293F661700E42796 /* PixelStoreTests.swift in Sources */, + 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, @@ -10825,6 +10918,7 @@ 37716D8029707E5D00A9FC6D /* FireproofingReferenceTests.swift in Sources */, B6AA64742994B43300D99CD6 /* FutureExtensionTests.swift in Sources */, 3706FE5C293F661700E42796 /* DuckPlayerPreferencesTests.swift in Sources */, + 1D9FDEB82B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, 3706FE5D293F661700E42796 /* FileSystemDSL.swift in Sources */, 3706FE5E293F661700E42796 /* DataImportMocks.swift in Sources */, 3706FE5F293F661700E42796 /* CrashReportTests.swift in Sources */, @@ -10845,6 +10939,7 @@ C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, 3706FE6E293F661700E42796 /* FirefoxBookmarksReaderTests.swift in Sources */, 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, + 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, @@ -11118,6 +11213,7 @@ 4B9579542AC7AE700062CA31 /* DownloadListStore.swift in Sources */, 4B9579552AC7AE700062CA31 /* Logging.swift in Sources */, 4B9579562AC7AE700062CA31 /* CrashReportPromptPresenter.swift in Sources */, + 1DDC84F92B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 4B9579572AC7AE700062CA31 /* BWCredential.swift in Sources */, @@ -11167,6 +11263,7 @@ 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 4B95797C2AC7AE700062CA31 /* CrashReporter.swift in Sources */, 4B95797D2AC7AE700062CA31 /* AddressBarTextSelectionNavigation.swift in Sources */, + 1D01A3DA2B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, 4B37EE7D2B4CFF8300A89A61 /* SurveyURLBuilder.swift in Sources */, 4B95797E2AC7AE700062CA31 /* BadgeNotificationAnimationModel.swift in Sources */, 4B95797F2AC7AE700062CA31 /* HyperLink.swift in Sources */, @@ -11203,6 +11300,7 @@ 4B95799E2AC7AE700062CA31 /* EncryptionKeyGeneration.swift in Sources */, 4B95799F2AC7AE700062CA31 /* TabLazyLoader.swift in Sources */, B690152F2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */, + 1D01A3D22B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 4B9579A02AC7AE700062CA31 /* InvitedToWaitlistView.swift in Sources */, 4B9579A22AC7AE700062CA31 /* SaveCredentialsViewController.swift in Sources */, 4B9579A32AC7AE700062CA31 /* PopUpButton.swift in Sources */, @@ -11233,6 +11331,7 @@ 4B9579BD2AC7AE700062CA31 /* ChromiumDataImporter.swift in Sources */, 4B9579BE2AC7AE700062CA31 /* BackForwardListItemViewModel.swift in Sources */, 4B9579BF2AC7AE700062CA31 /* BWNotRespondingAlert.swift in Sources */, + 1DDC85052B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */, 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, 4B9579C12AC7AE700062CA31 /* RecentlyClosedTab.swift in Sources */, @@ -11251,6 +11350,7 @@ 4B9579CE2AC7AE700062CA31 /* CredentialsCleanupErrorHandling.swift in Sources */, 4B9579CF2AC7AE700062CA31 /* SafariBookmarksReader.swift in Sources */, 4B9579D02AC7AE700062CA31 /* HTTPCookie.swift in Sources */, + 1DDD3EC62B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 4B9579D12AC7AE700062CA31 /* SafariVersionReader.swift in Sources */, 4B9579D22AC7AE700062CA31 /* LoginFaviconView.swift in Sources */, 4B9579D32AC7AE700062CA31 /* FireproofDomainsViewController.swift in Sources */, @@ -11272,6 +11372,7 @@ 4B9579E02AC7AE700062CA31 /* BookmarkExtension.swift in Sources */, 4B9579E12AC7AE700062CA31 /* PasswordManagementCreditCardModel.swift in Sources */, B677FC522B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, + 1D220BFE2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */, 4B9579E22AC7AE700062CA31 /* NSEventExtension.swift in Sources */, 1D26EBB22B74DB600002A93F /* TabSnapshotCleanupService.swift in Sources */, 4B9579E32AC7AE700062CA31 /* Onboarding.swift in Sources */, @@ -11313,7 +11414,7 @@ 4B957A032AC7AE700062CA31 /* LocalBookmarkStore.swift in Sources */, 4B957A042AC7AE700062CA31 /* BWEncryption.m in Sources */, 4B957A052AC7AE700062CA31 /* StatisticsLoader.swift in Sources */, - 4B957A072AC7AE700062CA31 /* PrivacyPreferencesModel.swift in Sources */, + 4B957A072AC7AE700062CA31 /* DataClearingPreferences.swift in Sources */, 4B957A082AC7AE700062CA31 /* LocalUnprotectedDomains.swift in Sources */, 4B957A092AC7AE700062CA31 /* InternalUserDeciderStore.swift in Sources */, 4B957A0A2AC7AE700062CA31 /* NewWindowPolicy.swift in Sources */, @@ -11323,6 +11424,7 @@ 4B957A0E2AC7AE700062CA31 /* UserDialogRequest.swift in Sources */, 4B957A0F2AC7AE700062CA31 /* DownloadsCellView.swift in Sources */, 4B957A112AC7AE700062CA31 /* PublishedAfter.swift in Sources */, + 1DDC85012B835BC000670238 /* SearchPreferences.swift in Sources */, B6B5F58C2B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, 4B957A122AC7AE700062CA31 /* FirefoxBerkeleyDatabaseReader.swift in Sources */, 4B957A132AC7AE700062CA31 /* WebViewSnapshotView.swift in Sources */, @@ -11437,6 +11539,7 @@ B6080BC82B21E78100B418EF /* DataImportErrorView.swift in Sources */, 4B957A722AC7AE700062CA31 /* FaviconHostReference.swift in Sources */, 4B957A732AC7AE700062CA31 /* DownloadsTabExtension.swift in Sources */, + 1D220BFA2B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 4B957A752AC7AE700062CA31 /* ASN1Parser.swift in Sources */, 4B957A762AC7AE700062CA31 /* FileDownloadManager.swift in Sources */, 4B957A772AC7AE700062CA31 /* BookmarkImport.swift in Sources */, @@ -11458,7 +11561,6 @@ 4B41EDA52B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 4B957A822AC7AE700062CA31 /* SyncDebugMenu.swift in Sources */, 4B957A832AC7AE700062CA31 /* AddBookmarkPopover.swift in Sources */, - 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */, 4B957A852AC7AE700062CA31 /* QRSharingService.swift in Sources */, 4B957A862AC7AE700062CA31 /* ProcessExtension.swift in Sources */, B68412162B694BA10092F66A /* NSObject+performSelector.m in Sources */, @@ -11476,13 +11578,13 @@ 4B957A902AC7AE700062CA31 /* BookmarkManagementSplitViewController.swift in Sources */, 4B957A912AC7AE700062CA31 /* CookieManagedNotificationContainerView.swift in Sources */, 4B957A922AC7AE700062CA31 /* FileManagerExtension.swift in Sources */, + 1DDD3EBE2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, 4B957A932AC7AE700062CA31 /* PermissionModel.swift in Sources */, 4B957A942AC7AE700062CA31 /* PasteboardFolder.swift in Sources */, 4B957A952AC7AE700062CA31 /* CookieManagedNotificationView.swift in Sources */, 4B957A962AC7AE700062CA31 /* PermissionType.swift in Sources */, 4B957A982AC7AE700062CA31 /* RecentlyClosedWindow.swift in Sources */, 4B957A992AC7AE700062CA31 /* ActionSpeech.swift in Sources */, - 4B957A9A2AC7AE700062CA31 /* PrivacySecurityPreferences.swift in Sources */, 4B957A9B2AC7AE700062CA31 /* ModalSheetCancellable.swift in Sources */, 4B957A9C2AC7AE700062CA31 /* FireproofDomainsStore.swift in Sources */, 4B957A9D2AC7AE700062CA31 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, @@ -11508,6 +11610,7 @@ 4B957AAD2AC7AE700062CA31 /* Tab+Dialogs.swift in Sources */, 4B957AAE2AC7AE700062CA31 /* PasteboardBookmark.swift in Sources */, 4B957AAF2AC7AE700062CA31 /* PinnedTabsManager.swift in Sources */, + 1D01A3D62B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, 4B957AB02AC7AE700062CA31 /* HoverUserScript.swift in Sources */, 4B957AB12AC7AE700062CA31 /* MainMenuActions.swift in Sources */, 4B957AB22AC7AE700062CA31 /* WKWebView+SessionState.swift in Sources */, @@ -11624,6 +11727,8 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, + B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, + 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, @@ -11676,6 +11781,7 @@ 4B957B462AC7AE700062CA31 /* Tab+NSSecureCoding.swift in Sources */, 4B957B472AC7AE700062CA31 /* NSNotificationName+EmailManager.swift in Sources */, B6619EFE2B111CCC00CD9186 /* InstructionsFormatParser.swift in Sources */, + 1DDD3EC22B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 4B957B482AC7AE700062CA31 /* MouseOverButton.swift in Sources */, 4B957B492AC7AE700062CA31 /* FireInfoViewController.swift in Sources */, 4B957B4A2AC7AE700062CA31 /* LoginItem+NetworkProtection.swift in Sources */, @@ -11726,6 +11832,7 @@ 4B957B712AC7AE700062CA31 /* TabPreviewWindowController.swift in Sources */, 4B957B722AC7AE700062CA31 /* NSSizeExtension.swift in Sources */, 4B957B732AC7AE700062CA31 /* Fire.swift in Sources */, + 1DDC84FD2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, 4B957B742AC7AE700062CA31 /* SyncBookmarksAdapter.swift in Sources */, B6ABC5982B4861D4008343B9 /* FocusableTextField.swift in Sources */, 4B957B752AC7AE700062CA31 /* RandomAccessCollectionExtension.swift in Sources */, @@ -11948,6 +12055,8 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, + B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, + 1D220BF82B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, AAD6D8882696DF6D002393B3 /* CrashReportPromptViewController.swift in Sources */, @@ -11965,6 +12074,7 @@ B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */, 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, + 1D01A3D82B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, 1DC669702B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, AAC30A26268DFEE200D2D9CD /* CrashReporter.swift in Sources */, @@ -12088,6 +12198,7 @@ 98779A0029999B64005D8EB6 /* Bookmark.xcdatamodeld in Sources */, 85589E9E27BFE4500038AD11 /* DefaultBrowserPromptView.swift in Sources */, 4B4032842AAAC24400CCA602 /* WaitlistActivationDateStore.swift in Sources */, + 1D220BFC2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */, AA512D1424D99D9800230283 /* FaviconManager.swift in Sources */, 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */, 4B0AACAC28BC63ED001038AC /* ChromiumFaviconsReader.swift in Sources */, @@ -12108,7 +12219,7 @@ 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */, B6B5F5892B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, B69B503A2726A12500758A2B /* StatisticsLoader.swift in Sources */, - 37CD54C927F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift in Sources */, + 37CD54C927F2FDD100F1F7B9 /* DataClearingPreferences.swift in Sources */, B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */, B69A14FA2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */, 1D36E658298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, @@ -12250,7 +12361,6 @@ 370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* AddBookmarkPopover.swift in Sources */, - 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, @@ -12280,7 +12390,6 @@ B6106BAB26A7BF1D0013B453 /* PermissionType.swift in Sources */, AAC6881B28626C1900D54247 /* RecentlyClosedWindow.swift in Sources */, 85707F2A276A35FE00DC0649 /* ActionSpeech.swift in Sources */, - 4B0511BD262CAA5A00F6079C /* PrivacySecurityPreferences.swift in Sources */, B6BE9FAA293F7955006363C6 /* ModalSheetCancellable.swift in Sources */, B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */, 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, @@ -12408,6 +12517,7 @@ 85A0118225AF60E700FA6A0C /* FindInPageModel.swift in Sources */, 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, + 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, B6DA44022616B28300DD1EC2 /* PixelDataStore.swift in Sources */, @@ -12427,7 +12537,7 @@ AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */, B6C8CAA72AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */, - 37CC53EC27E8A4D10028713D /* PreferencesPrivacyView.swift in Sources */, + 37CC53EC27E8A4D10028713D /* PreferencesDataClearingView.swift in Sources */, 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */, 85707F31276A7DCA00DC0649 /* OnboardingViewModel.swift in Sources */, 85AC3B0525D6B1D800C7D2AA /* ScriptSourceProviding.swift in Sources */, @@ -12481,6 +12591,7 @@ 987799F32999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 4B379C1527BD91E3008A968E /* QuartzIdleStateProvider.swift in Sources */, 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, + 1D01A3D42B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */, 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, @@ -12488,6 +12599,7 @@ B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */, 85378DA0274E6F42007C5CBF /* NSNotificationName+EmailManager.swift in Sources */, + 1DDC84FF2B835BC000670238 /* SearchPreferences.swift in Sources */, B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */, 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -12504,9 +12616,11 @@ B69B503D2726A12500758A2B /* VariantManager.swift in Sources */, AA97BF4625135DD30014931A /* ApplicationDockMenu.swift in Sources */, 4B8A4DFF27C83B29005F40E8 /* SaveIdentityViewController.swift in Sources */, + 1DDC84FB2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, EEC589D92A4F1CE300BCD60C /* AppLauncher.swift in Sources */, 4BA1A69B258B076900F6F690 /* FileStore.swift in Sources */, B6A9E47F26146A800067D1B9 /* PixelArguments.swift in Sources */, + 1D01A3D02B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, @@ -12570,6 +12684,7 @@ 85707F28276A34D900DC0649 /* DaxSpeech.swift in Sources */, 31F28C5328C8EECA00119F70 /* DuckURLSchemeHandler.swift in Sources */, AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */, + 1DDC84F72B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, 1D43EB38292B636E0065E5D6 /* BWCommand.swift in Sources */, F41D174125CB131900472416 /* NSColorExtension.swift in Sources */, AAC5E4F625D6BF2C007F5990 /* AddressBarButtonsViewController.swift in Sources */, @@ -12586,6 +12701,7 @@ 4BB99D0226FE191E001E4761 /* ImportedBookmarks.swift in Sources */, 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */, B626A75A29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */, + 1DDD3EBC2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */, AA6EF9B3250785D5004754E6 /* NSMenuExtension.swift in Sources */, AA7412B524D1536B00D22FE0 /* MainWindowController.swift in Sources */, @@ -12596,6 +12712,7 @@ B64C84EB2692DD650048FEBE /* PermissionAuthorizationPopover.swift in Sources */, 85378D9E274E664C007C5CBF /* PopoverMessageViewController.swift in Sources */, AA6FFB4624DC3B5A0028F4D0 /* WebView.swift in Sources */, + 1DDD3EC02B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, B693955026F04BEB0015B914 /* ShadowView.swift in Sources */, AA3D531D27A2F58F00074EC1 /* FeedbackSender.swift in Sources */, B6BDDA012942389000F68088 /* TabExtensions.swift in Sources */, @@ -12616,6 +12733,7 @@ 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, + 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 4BE5336E286915A10019DBFD /* HorizontallyCenteredLayout.swift in Sources */, B6BCC5232AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, 4B92928B26670D1700AD2C21 /* BookmarksOutlineView.swift in Sources */, @@ -12679,6 +12797,7 @@ B662D3DE275613BB0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */, 1D3B1ABF29369FC8006F4388 /* BWEncryptionTests.swift in Sources */, B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, + 1D9FDEC62B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */, 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */, AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */, @@ -12704,6 +12823,7 @@ FD23FD2B28816606007F6985 /* AutoconsentMessageProtocolTests.swift in Sources */, 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */, 1D1C36E629FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */, + 1D9FDEBD2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */, 4B434690285ED7A100177407 /* BookmarksBarViewModelTests.swift in Sources */, @@ -12756,6 +12876,7 @@ 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */, 98EB5D1027516A4800681FE6 /* AppPrivacyConfigurationTests.swift in Sources */, + 1D9FDEBA2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */, 4B9292C22667103100AD2C21 /* BookmarkTests.swift in Sources */, 5601FECD29B7973D00068905 /* TabBarViewItemTests.swift in Sources */, 1D8C2FE52B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift in Sources */, @@ -12801,6 +12922,7 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */, 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */, + 1D9FDEC02B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */, B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */, B6619F032B17123200CD9186 /* DataImportViewModelTests.swift in Sources */, @@ -12855,6 +12977,7 @@ 56D145EE29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, 4BB99D0F26FE1A84001E4761 /* ChromiumBookmarksReaderTests.swift in Sources */, 4BC2621D293996410087A482 /* PixelEventTests.swift in Sources */, + 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */, 4BB99D1026FE1A84001E4761 /* FirefoxBookmarksReaderTests.swift in Sources */, 4B117F7D276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift in Sources */, @@ -12868,6 +12991,7 @@ 857E44652A9F70F300ED77A7 /* CampaignVariantTests.swift in Sources */, 3776582D27F71652009A6B35 /* WebsiteBreakageReportTests.swift in Sources */, 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */, + 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 4B723E0826B0003E00E14D75 /* MockSecureVault.swift in Sources */, 37CD54B527F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift in Sources */, B6CA4824298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json new file mode 100644 index 0000000000..ba7383a80e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0xC0", + "red" : "0x21" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Contents.json similarity index 78% rename from DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Contents.json index b899856ca3..313962dece 100644 --- a/DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "rocket.pdf", + "filename" : "Icon 18.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Icon 18.pdf b/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Icon 18.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1f47d51258708f01c2427fa6f1f5f45250c3a497 GIT binary patch literal 6134 zcmbuDTW=*t6@}mXSMROXuI{l% zpn#v+>(t)0uWRq_SMT0_``Rvfn(S=m`@j5kn&+>+n!o;fI^Vt5zmq%S7r#5-eSG?1 z_JEf<_4xE~zPg<*-k$$)eL7$M`kVR9cgO!ukJH~LyO{Pjo9*HCczXHU_~~>znI(j5 zVns5R;BDZcCZIGwp6>X$D=yoEL^@{O5^Qm~%pPHzottgiejEZjbecW`-b}li{pjIo zT(VXZrD&W%pY0?axidUH`CBDu<`Knr=j$;$7x_U(+|^!>2CVt|Cuu9 zT#AWI1#5FEN}12mn3l4U?{V$zULRqxJ{Xhd@$mBaK!~ngN{Y4md4M)<8QxV4Xk3Zt z01rxbb>r6|vJ0H1xF*p+OS3>_x|JpS3M;C9d94vr-5^*5lQ+$gN_(1Ug zWx?%t4;4FfxN-+~C5NX`+(Tp+Pyjys;^&l@Uuvy1?@XF{UqcJy`Q?LXA?T^v~PS!Ja_nhwb3 z8*;<;Zq?8-rpT)MW=3q+e*lY^f8!81`78)GGFezBWiaMedI#1sqrpC-5D5`(S8 zRXOu^z5%PIK=Hz&uU;w?P_(-xg%V;uBGdsUqHCP8=ue(WAv*1n!a_{8NpewwWUTR~ zXr361$F6~_x?=WW$8;)tN9iJm`c*FZWIzaDAb>RD5`#Cv%&*;It1kqC39*2c;su6~ zY}QpVgyDmh3ZT@S0Tr2h=E5*$?G}ioq~sJ3{*i7XsybL zZxL;6<6|){wQ!VJV`>AK?Bt|G2NNk6%0Y++p_&6K%+lm6x}w~&v1}I}mC=_lI0kADQ0}M5Iqb5oi7H4X0O6Kg=9Gd-6^gBy z5Nl=>b@Fi2Fqab>LCcL(P1hPcDH%Bxl5&f4&-<#2N;{ldCk086oNixPM71X*QR6Kf zqaIuJ#TJc$wHhswtTu+U)v*vqFVJ4S+LhtFwc+TB8?d6LQ?d5mfqfaJ1h)5=n=}4I z<9i$CTAQxA^;~b(ykz6DGkZhlrFY?Id&R+cx=`Aa_1M^AYOJkPHyD8?05Cu)Hi`;% zMB7_Cv&w0NJ*SnQy@*gdDm8~KeF6{E#nFC@ zokj;#BS>}y_S+E)_nEA+>rA3!j4997(>SZtnBf^_V?WnwrJWj(Dx11dukYuwIOtPt zJ9M~pfU9v-T3WEWKw7H7*3@Wpe)0Tan%LPq2@eQ4lDT- z(#j&D9c*0(CrY^-*71$IXqPA?IeJ0uYih1c6ukX?D43$kBrAvV{Y zXhUu5M+R2Ev30U%T6qmh*+sEw2qX`G>y)fVmu0TH(l%Q>Bp4vrg#b2qn=}D?*^PKX zf!NS0_7GQLXd~gU@reHoZAVO$-jO9{vSn=|!abF3bko1Yu4i1q;MAJlUKd*MOC_yp zl3qLXp%SiI=oKbopEsQPT+{%Gi-27GR z0&#L0GfJ;8BU^>B+^q=4tO)bwrJQszL^CrvU^^t7C^CjrZ2PR>IJAyR7uW z{$O0)w0o9|6=A?N#No|~d@4uJ6Rbvu_yZwed&4Bqe8h1vgQDCK&)k10CwkJ%ru191 zv;ZWH?26JJQPBYtTV1`zy_GamBgTZ!Jv%BYrlV`Mtqd|ebcWvE@=TZLK6=*j<5~~A zw99t(Nrn(uNyR~C8wPDQjMql&;uJ5ME8?4_&*c{hVCU)U8IeO?5jX`YPHY983(x zhDYC35t@T*NFLjbD;gs;syA5_n3R;T<_R^e^_-<+l@md;6N>p=pm{70B#^cfV(53( zcf*(v9c#xq716$);&ttFazuuGj{8`9sLr;xH!RW24NLsk5GF=H)#CSDu2H+Ez~v#m zt>sjEDwJf1sW9kX?ZzgfraU5SZ3JYF}{pZ)F|O{OyrouB=fW(bDTCZ7$ne zOe3+;cdi7_gZ0SMZi=$}>Dl2mK`}K#)*OoOct2uOUbCNg{m@8Y;$WFDs>X|@j@8EN z=n+v(5|Fm#ZR1$A%dBf7ZeUb6^Fz8=$QwDW`;ZW?TpxNxSls%qZB0UQpy}7HqZ5S? z)Z7r-T%+;je9+V&w|zqz>(K2ERyd31Rrlf=@08T>XyYz85V^@bFwo1uWJp`UhNgiW zx4t8onbLgg8oF2npI^B{D!Xpk4jz5#+Ky1ycKC#%Pqh8}>-Mo$-(}a2xCFqDZ@4TL z`?uYTU*6r_KRwRh|Cw*S{9XL%pMN{g7w@hGJ$||Kr2y{5b1t@?}@|=Kk*Ke8-LYl)QoOt{%W<)}RI*J~#^+AEGaQd;K0$ zXL)crZ}+^p{}EaH!v05)gC_QugC;rnI4;~Be{=P8b#wpmQ_-Iu&L1WkYg)Z-o@)@s z8Myz!742!}Lv;UR41Eew#NwEe7nk?EFMYm3GK@YB0*|=ESt?d+Pe|=}D;Um4i zv^p5{6X(sF;S4F>+~2)>WxA-8(Y-$W_Pf&h^=tj+jhgol{8tSvzWQO_Kb}8nOW>Mk zozBO3vs0_P`LFFXufKh(Z{N57rjz&7v;(d*a+Z=$oBY#4OjGnjlojJ+l(4c4ciQ&B)r7}!yTCN_NzJ}J|8V+h9B zIkO9k-{*(ghtSk$qG%% zcGkxfTr;n(f$4%9jOFZR%bsZ$j!6Ln1qOJ9=|Cu%*Q`|#C@KUFGHqj#@S2F*BwVik zq&}&SU#Y$N`M;N)L|m2cDCFp=bb87M7m%fL@A_aE;j+V2tS_4gj~O~=(h!rjolhnX zgEpbOgh7RJm3Y_p>0+LtA|FRyEna9^x73+nLhuVj-4HdK23onET+qCf++g)sd=ccs zFj7ynkXed;QWd23cf&yaLMtYt`qFn|0Vpp|W_B0M;$I}?f~3M_osgbQYdjKSo1N=K zNLtAh^J-#|Yos3;2Ew&33jqv+>sKJ)TEd@Nkvru&V`;r&(@NOID1|BbaikV4>i`Z4 zpt*4sGtwhVPY-rKY%3nZKdya~Wyc*sY63+{< z4|%WT0o9wlTR6xd*Co~}`--MbQFVVUvj+438`x5>vR^%v0i7Gvx6v*QY=`JW*gd(^(grEvcP!4}UIBFWiaayo|cu}Ix2d9WqDU<1;q2L`QRY7k@# z81*W5l%sMK$F4|$KOnqc$aG*(4kjRokE6yPxX$}laV{kUT9FfVgFrnnkz;9==(=_e zB!6<0<-W_-imB027qM9}tdOKJ)mVBA1YIW8urma+9%UbhyY+ z)71;k>g)afa6ah|ztV+=p4BgZ|1s;;{pN8N;K%u4yLo%~qCfE2k)JxK-y(HXnAY>@ z@cB5+C(T!?wb0!j_UCy|9{E01!28XSFzp3uZe0xFHn#eH`#@4PN3hn#&+Xwe*_tc; zCqfz~`lxT1IKUGe*1&H!=grgM@zV6masH_2E})k1`Bw+;t=9z0W9TE9A5EXd@>tTz z4HFy!wPsmA2-{l`Kai6y87;p((&SS*v&8Xi|xb9B`K@*;c(U* ea8B^*{q~=k)aPsJ=6KHG*kG)>x%uX&cmD$7ANC*s literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json new file mode 100644 index 0000000000..e7cd622579 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Icon 12.pdf b/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Icon 12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..671d4a0b39c46b9cd5ea853f3391fd7a47995818 GIT binary patch literal 6306 zcmeI1O>Z2>5r*&cE9N4=4&Y&be*lJoSc-!n38K)s89p$pwaj3}O-MR$^6T?dPftyA zNf?ljqvs%v-tDfgs($;en%1kgZ@&4$E_s^lZ07Ud|1r(;mtW3beKj5L-pM~FH{w@+ zI^Mm1{Bibx*D~ti@&0)8VY+&A{P*qQc>VjY=hxqk|2jNO|C;P#+B2K&t+ozyZy`IVK;i1@)m`TI_((t*H{P4EiM>W+?Rb#d#OJ_^Mb~ObRJRd{rzh#`zG*3+Zw-HLHC|>$WJ>#lvK=94zaz`Zg{sKBrvW zoR$)5v}U$(n;MtmtIGyQc2F~U8bCbg8MhcD_e|P%i*;M}^MGOU`L>>>AEzIFGu=&p z{RBRh>Y{A$hmnaAfK6mE>~bGB@JU0E)V&m#?0I@N zf7p@1YTrA~&A!&S@!j(g>H&@fiEMV_@DQ~a6n0tI7084q#({%9`y*F7ggthoc+a`W z+b&g@g-QgB5t1vOBZKu~{2Z2Ai>Y#Co+sgxSe0yap0!P>^752bHCPMPkgJ_l2@#8_ zL}3d|B4%M#696;E#X*iFIpM69?7d*s=R%$^Q+!7LNRlN&j>!_LiPh`~3Gs_BL_Z8` z2^OIXX&*_>lQSN|us|gWavnI_P^RwpCo#;++i%^NVHC5-5BQ>m&JM|(ycN`FqdgN zhzNX~2J|ee=CztqN|9p&zk_E-#EZ<|rfC2}dri?vtq$1|OPO~@>JEyU6|%oZBMXw5 z`mCmOyB9U3u4buo+lv^$gu0>Yi01bn6%)fmu>l!b+dHr$sLZQaho|?S~G_p23P)Wx^&DtK3oNX1wx;G-K`#>0jl0Zl- z#wA%7qlZR5*65v7FYGxAYZOV3X#NU;A`>xXI4SnpI01E$X0JJe0OH}g-3;8`h_yW; zVq1KQ9fl22@ROuxUBeMtOs z8LCrU$<@KaR2xkrm!o)9lpNV&3G`SNp@EC^P*%W^hiy> z6)Xs-4Jah;WkfSe6BuX@3seoXUbHSE2rd?A3+9kCPPb@lu?C@?g^9r>Y3VEtG5!c* zBB@Js4vvB%^EkHrbeJfelrs*DREgriq#{+`k!DT3lr}PN)9Ixt)FSh&5=5BT0&HeNQo(i_m+4L zB|`-RjFR}^YM^NngwGV|ST4hrnnvxVtEa7}8Ss#(u5D*^5V+|zZIkerL+K?U61qA8bm^*Ef6*!SRnGr#?W8O z>zaesYi+vrYk6(;_5WI4Kc-cb)7>8Qub-Ai=9K9abJ@AiAl#?&{4(o zYoD8q80geFt$iM+Vf}bVrZ!9+kptg-(w70#xdX=MGA* z0cz_bx?1Y>I(gV8=p7xmGHg@ei`m9jePtMjq7PU8O51aZoxr*Y9mm&xd*q;q^++XG zq`yd4&|h^SX~#O&-I4F{ZbBng9~2Qe(0Tnuwy8xVt=Hv`8m#`L28Py4^CjD)f}>=V z^U+DAF9XqD!gMYj)i!s)^wLhQllxuqcCjpX$m@l3OeyMpGaI+xLtp*&?(Y2fFn{+w z_tHF9fBVlrkMq^roA<{K{Q3Cq_U7yJ&+~ULWW{P`_qWZ`_3`2S)BWN2Fms~k>u&D# z`R?&}$BJ_8y@79U?!jiRFd8&mjtiuh=2w5beTS>FL~uQC>%2by#9aH#{|Au45}P_$ zk^moc!9DQTH;*@`^ZTb$f4)EdFqt_|D=Fm9nG0dAVSxL??QMIT?-b(7?I{$q+|tOS zqhij~zNZkMK^G7eXP@|Ze{*{}-cS55!qp$%kdDtD&L57S&!64C`+QBx)%E%OI7`4S e!B^kj{`ZLY?SB()?jKt?$&@g?diA@%eDgn?l-5E3 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json new file mode 100644 index 0000000000..9a9c668121 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 17.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Icon 17.pdf b/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Icon 17.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bf31e9a7386206c8c9b2d7fd8270ce01bfc9d1a7 GIT binary patch literal 33137 zcmd_zORwhHRp0S>y^1qIf&taf{UTYGzy>QyB8Z&<63U<{7sjM7PInVQUVT2_wg1o4 zUFQ@eWSSb-r+;VP_qwmO|L2eX`j>zCr~CKUcklKuyO(eO?jPU1y!`BEFJJuEfBD15 zmoNV2um0{IKYsWD=HD0p>Ek!Q`{5s64jTS?gMRhhxBv8qpLCf2UcZ|@tK)Bs_uZG@ zeE)|pzx(*jhkyCx_doynHq>wa>F<7+N&oWO@4nu!Uw{1m+kg7*!^iKN;@R@g|NPs3 ze)-#%-TU4A`|*eU&5y>}UvJ0LizzO*+wS0p^Yya7xsmtB!+n1{ zzZ~D+PRHYA|MKCTC0*~w{bB!dH7abU{c(S~oQ<%1zu#XD{qz?4V1J)!cRrr%=jCql z8T%jim+V9W1>keYS=l%UN$cJ}tTiNwt zce-4Re%J4?)9-%W9WLjW{eFf5eQ;ox-DQ7p6sPxx%jISjcjtaM?JhyLixu8J2Yq<= znRa%v+aE6Wc6@){&uF*z*WLYa-oNY*@Apo?Xm#Bk&iAWX_wV<+-Tv0GoKI)7b+nfS zb;yUq;dF2Q-R!~9`t|*Ga}L+{*A{$TJ~Lro4UAUw1Q~siomW{ z$Bfc$h_7}I?|i-OBlz>-cskxp?-BJMFq99& z0sG_CM4gn&?}}a{AKtyiZ0K_8`ml1>*A|aw1MOc(hacxV?T>Z@yV@gO(5l0_Ip||& zS-<^#x8ENjx7#t+YrgaSj351S+w_GDIUcmSKl;5zKD>L2q1D~>ewiECkw;fcxgaMr zc6on7?r8DDJ1j`bkVohD#K2->>%2g=^K7Fh;^uI4Qpe8jtRL)h$8O1|g`Mp0ej-R* zDn@jbZ-wQDcmMG2@85m%?!WxIWi(-Va$6=GA&oAHG*j*Gmz^_37Pm{6@`;ch=6CIc zwC&xFSHdYFjgZeTU%}iXW^2XRmRNB@_J_<90w??cacQIq~Tior^BxEIiFBPy!Jeg=eoXf9#7S9dVf!VrBZ02-RXR6dN+5XH@^Bz zyL$hq2@)%pBg*+2sQVs0VbKG5M6-MaywVG4C*t|G-_80j`_t)8@9a~7huzU~Plv-1 zxdvTNnAyUOLhdGG4YI%7`zif*kN34Zk?ftB^`8^n!?pyLvyh(Jdu}J*M~lO8cgd?A@8`t=*Y4x#4U|`*KWtC5#D+X>~%% z2$@K?YTS4`IP4*Af}q>Svxj36^O%fCVl^WzvWJ|ngj4;leuLb9q9vc}TC08N#nG8- zxrwjqJ>gC&TllUk9uNU;;AsQjbZ^ujwc|NGA7^|Gyx4*}+oR<3^-S!31-u?H2L*7( z?8l{VuZZHT9Yc$;a7aI1jz@Ac=&-w<(cu!*XA~GnDx8rzDRj76de8~c5EOTc7d5ze z5UXee?q}B5vVT<9lvV~*%O+9}ccL_%M-^RZt*-))`~B6jf#?u%20pa?vwNd1T`??4 zkvPmD2`FCPr}{{b^L2k3J_vR5qpe83-Tq?W&yBE+d|5gj#kjIrZ;f|%axbD`k2r%F zVRyY=5CTHG*p7}8vEjY@p2Na!rQACbcSQPKLI`}jpA#6MR~3w%)UQqdpBeTA0%{x{bov9dF06S6P5GP!jHNxf2@<8w&kE;p=%NDXwE| zp~-Pmc2wNGHV@_@O)vrOP2}y3X2&*uMQr3E)#9j$@~fk}QN=fM->Hmyu?)=Tx?1hi zRE!^ppcMJl74Yp+GiU+H&W-K37jr6hm92Nh*cX<_QYt5W$wDV#um|ss`+8LjsII9s$tM!7rE9(OQNhrZ4n zh|6HkCed8G8)6H~my2WKs4;?{dX6PL(BvaYBzx$X~VtYw(p`Lt2-mHEkO^-B_B zxaDacKl?V~)6hjT$J?g&E_yNG$OD$sk?|Q-Tu>h?huDei$&l!SPG#;+yZh}vws5iw z$!LXSE~nfvw|}>3Vi>iud>K3`N~#rCWr^lm1*JNtE4I2={!a{X?Y8x`?5FL`%}zgc z5cMCo36-!UjM$=KvD{#}@!6@H-Tv&`h)=7$9|-uH5f9`q&d(QT#%1|_wJ=-}w!iT) zVQ>4cH6Cf9wus$be@58}BOREcFp~E~JJ7~=)SusE(^pH)Ex0GlGG&ag?7j;D?rvFJ z9r;Kj3XKp)|Gv|hccUNvPNGt)14n)Y>ag(yK2+Ba@mXQRyVYtOO{ETcRBE%U}GFn~O zGii%JW{wW+MqI4bB@W$1&~2X>4Wj=yh$>mL#@gilv=eYkeSyYIt*aoIUxHX6TdN%U zHLF|CE2$k>*@CspZeP$9Jq={Dk5=0Or?fXtzfpBXDVkzMDU6m^HBce4HClW=d)?dW z1586rJL+R=M^$VP;k~-%ZLO}PI{?yDG zLoCA(t&y2^6@TS{O}k5ejjlIy#0R=EEXdKwML=fFiZ#&YbS2AK`?X`uOjs6=qRa!aDUGFT zEFU5GiSKAhg{RRnj%2UzL^RrKF%OQVJ7#pbIZG*KjG5To1kT>^+=5<9%-QRk0K&f# zEO1t&>k0xebg0aZk5Y{9$f;ZaFkLCS-0kVgaa$L`ZDmW;&N(a|5&ej6 zg6Oa{8kvJfR#&dq22v-EIwR+@WGB)f?iaL)zXO3_s@0Xvwsl2ywpiDF^2F(j5WAa! zc4?%6&SdS@brXQvO(3PNH2l_%x4d;NDluIdMxX2YX!qEd^{%}#Ft@ew5tf>4icglZ zR@dRI)s3#J)naOEJFm>aY;1|$;*-c~?R7CkP!#z&HJK25O?L%6X%P`yf)Ya(*g$o~ z`SM}2IuXo)NlYsg`r{cL1rJJxF@nyn1SYRQ!7^bbO`JLJgPCDvIsMK)A&e_R>SxiU zCd>((Y!7mi>8c;ktl%YEoRV=E5Kg6u>qpzr2VuGq=n&CU2Nfb-KmEwwHBu>yh(jx? z#j$IZv}jdAKuOJz)16=r5%ld0c5FYyI{kzJ-T7d-!L^dwS^Y*(hTI(Kv^fZuZvF0i zBD;RFDrT=IcW@3dS|B0d^V5%>$j(sSF3d0AUCI8zmYS8xUQ`q3u;b} zaIvjvwVjl-Cc!Ys$|xCo`Due?md;oN(`xle82D|lxIpcJZ$V{CjuERYhCxaK4U(c6 zqsq|%tHLUwIN*AeD@UU^eZA=KaSqSC7d0-GKiS+YU9u! z{c-}QA5Xhhmdg?uM3!?QDY_6YZTDU)w)yIJjjSE=T8AZJU(1~3fp4kvXSn}sJ9?cKfUtWVT(N5ssgr>0hX)~%MJEw9 z_gZLk&KbhHoPMm@ZW?lQrj5kkS|CFINx`%C-Z*#3H^+nF3Cn?a#|x9r>HP zWn82s?C>Q1!~l;96VK;9>QiwwIUHZoz=ucFd3b87txQN zQ$BOEFy1(8&w7PA2xA=qi({=Hw?JeUjoGQHbLqrjLX|iHHBWk>R%DN}fVd7$w}1*E zG)OURS+n{H@@`o6RiitA6s4FT(P7^ZbUW!Y@eCbTztWN2TDL%8YLx}lT?-YK!V7V3 z3lVErx{&{mS87!{VCJLaC&pXN4k1v#PH5V=wMRc9V^$?(G&=3V3e~)kqyT1}bSa?& z5#PiWvp*5pq0)wEg~8pgI>{YsurD~9TZjP_6z#Drh{ zWQ;~S!0feF!lg`#`jv1-fv+V`nf0NrQkjdoPLrJx-$K6;CORV^?OW6q-R8H*O-TB) z1Vt8^4tTzr9L~p3gt%+>04xp7z-oUC(s8UMv(*$W!IQ}~t%|alsw@GWR#l0v0;4P_ z9= z+`u?AX!+FBE{a;vwyhJ}^f9aC&$c>LIi*OtmVjSf>8R;Cf@DEk4#HC6q^%u>n1!%) zAFFe)3o5L#7Cc7(fFgSjaOp!E%jjP145HOmru9rPx7RnR-huX+`YZdp?)5;G8f~C5 z1=CeEPtMc=?OIpPwUgK&Og4~M%L8JPZ(XB~)s6);?FgI4XzB$PG1QVDKQ3~Iu2qA_EQ=;Ez9hj&Wzk8jwUgTk}b0$2h_WF^k;^;bt_w2UfMx>a5N9PZh zq3VZ(Vh=jU#v{@4o3-mE=Iq!`I6jEEyJl4r%IKN}7r!VHTBQ)ORn^>#?nh8{=#&K1*n)9EBj3ZJKAcTwaErCp^f`M2u94e{`B9uq;$FdE;EqH@6@Ji%5tEs^1RW;|It-2@G&dHWM*~g5zchAk~OQ6y#>im zu31w%kFFw-tDPfxw9At>t4(L`&R&`53#yFyvDf^j+S%+jE3LHJRnoC_t!RH*E$Uh0 zRo61$6n@VXOJNzT$iQlqnNcfO{o@H&dOV=Ii$}Y;f9wTZAnOh3OjnuW1&uW} zP_-L%oizEQYvrbaC1AF$1&V{D2eI39b^Qam%OU?jr`g#FKaD0HXgQMXfvM^kXnF4$ zc@BM>RqaabqzHxqyQZL(J9GpzDf7Bw$OS3-n#&2Kydnuoa?7@$=Gz2_vO8CL80jjo zsTCDQv~07gx|s!YuOnEa;X_$bS6$KxNesl~1);oZSXbBLMPnym@0Gj&lp7>)g`o0J zWQ^p&sK2aY!Rc*QhFgrqkhjdNaY-~u=Fwr|P@}>VpFGR?WtF58olVld5?INx&ye2K zUcN5gY(31zMFa}k^Me;e=DJ+a1dktJCVAo}9k|+2kqg>(XEge2pyE_>Fik;xF0kOy z>grAhs^)5g$Z`aor>}NJ^fu_x71V2)%)aqM9$ja3z>L5(D`Hu*rcnpN zt_w=EKNc+2I3PkBp0bCYnbftw(&|det*-3Cjo>TV;t^}d8c*^@jg-zTgvI;hcZ^=E zsz9F1T@fV8*dd#=T6CsSbE*8rFI>nrKn?4lQ3*cfpQ2&rdIGZOE$r#$e606 zD~+m3k|3L|_yU&##nclxMk|5qv_YKpO$o4FR++tg6Dx0Zbu3Fo`P~xRsnS`U0%L2R z(M&c*Q6)mddQE|A$9Up{6^L0-Ar!)bPmzIIE8&`vA0@OOb^()qBnSmnrO9fyJ;{;H z%juo2<%qDx>o3%08TSN4Ghv6Rz9Z32SXondxce5T9MMx)%7HBHwSI3CMj;SCV5 zv=CZQWLon4H?fW=C5-AVqQ4cfDVJNdx1lslvDJEWfM=B)t%6a5t@?o434sg&@e&F% zxxqn6iLGUIMhi-hZCL)^)>~lZ(K`*c)-#Sy>#^}|J?MFznl(Dzt+#g_%Os+G z(cAPc^R(u{M}BIJ6=rVO%1tbE9HrSebu}#+F|A^-F}L)FEnCd_#C>aS7IM3tmJ@K9 zmScM?tWbjcMnFj&Vz5<2J zm?9fiq3!gZ%;YvVOKIDW!V$Rg%Z6caNvG-H2dr_noQKsn*iN$;D&kUsmtZ z0aj+;;@eBFxh^l-wv}VYREOSTers+DxF;lGm7&duo+50OGekrAu30(dIpH!blf2HQ z9K8YveUc}|T1Gk`3w#+SSGkmsoEH_b&N5%In;+-02!NJ2RR}Q5Bon};R>@bQ+}tWi z$mwDg3=1L0+$X=c;NlHr3vW9*Pjzy7i+6aS^0bp)i@LGoK0}v8VAUU9P0jrj1RabMgT5?>rmRNVg3Sg|>o42MnXLT9(L5AicRJ|2^BYXT)50qETD{e0J+K0}TW_ZH>|15u1gYyygKrqx zSo@Z!>{OKSNM>+fw%$a}j42~fs(RLg!57BBj;f6qBg~CcfCXPxZ>m4KHKsUdC!i&D zzqqVCRyqW>o`=-Vk)y!rU7&qQ68;<$qm~G`puUzA=-emFYlgxC$sk4ZsEsInP_>|Ep&X$Ms;Xfmf91Fw8lQt7l0-(+Ojc*l zv|5jlOpN?^+T;DZ^j5&i16DmT4ht^G-lE#v2Qy1GIaQjBA!X%*qenWK<>%IBzyeND z1+DDdxp-PK(YMMw5yOemd+u0l$~nX>rR`R4Lk>^#kSc>48AKOWx2h#|SDk|qy_lK! zt@E;{his7G8~Fz<$Tc!(qA>OktD<98F3dXFNf-}qlA?`yPzkwOTKLkxvU7yO^pi?UpudMdR0)Jr-Ln87Yr>mQFTe>D|?C zGn8R*z?``iZP6#Fli6DK2xLYS?BCXdtREQg*?X$7TV*drx88D|TkpaMYi@b;2Zj$H zb90$CbTs~)P=V|<_q;v$jGQZgxu}xaGgMhYeHI{d57i3Yc(%kxEha5P&AYN9F?_;2 z#kk(TQPkV}x6u#j0!~d2ok}gc+97CtGIc~}+29-X@^jgLeW`#Jo&(1VmHQV!70%ek z!wR3QZ^ZV@&0L$g=b7CYu6&|SdN!-*yaSMC$nC2jm5nrZef2;tK_HQ-l-$FSuU^58 zuZ2+r;xwf2#-N+Rq2e**fcq|y(ta!NoBdKdn=pF&2(VzWV118?SVf7lj+%z*9S+iB z6|FEdek29SLARicYKwGl4c%_Z^Mn)y`N<2u=5V$6 zRx3oB%iMa4&>}v07UJ7h%Zs?~TlboMTm0KTt7&MThmGT$;H#CjV4>kx5BVk-kAU#I z0tcoC;l(LZoJ2QCDX|==04nymh|rKu61^OgGfAY z$SNnU75jQdcTc!Aq$^q@QBs4rh&d?}lND=!7;%k--DjO1OU26z0H?WTm8`D){133{nVv(qvp$Pc>A2Qey^Wb4nnLTKw2v` z8`A5Ag{&&Q!76c@R-l@~$_@gQ;#3^Rq9L>;}p=97}xMz1)_NaP*cc1DWR21%nq z=-+Xb!%*dhm4pb%uvo3CtqMWVTbmLJGoPr6%{P`)j~Z!ca$_O6ln@yslu#SIlfx4x zoIAy#OcCMJ1Jj+`M+dcsf<0kmgSd1;91_%Zrq(Y`5p;w~(f@#BsC#~z} zMORf7S_wl9x0MRwTR>zJhOEs8A`9q4tZv6t$+G+Z*DEe8Zd+DFfrsPI%IP%c_QOdly2q zPce42%G)}d7#U<1Gjb+O#=~*^8O1W&6LTZ|$=*yU-sf7U*IU)fQAB;9sQppr0=&`8 zGMgz<8VaY<8nQ3j{W-*HBC5M9BBoyjz5G+f53y`JR^P^_RDYxPA+vFD0Cn@p6Kq_O zGnF>$U`>S5c=qm86`;)we_^36H@cNES=_|CC`wMlIlYkNHw=N9|7M*}6Lnk-Qjb;eZlmtb_uGOWODHmca+(mf2X3o%to*yvxo(>ES$y z6%~sV}R#&-K>!ca?VKe zgmcZuG3-ti*_Obak>ndT#8MVQ=uR6|E!#SCEjG*zPAm4rAf93+kF81IZ6U&lkA5;v z8-|ysRgsrjd4bG@44v6ahq0}**CErYmo3xE(Jmx|`>o7b6$&IAf{3!%u!uZK#5=1{ z%~z=;Vn#;tmL*n0!`I$;jC3m>AJ8;ZUS{7nk7M1<})Fj(R>m zqhqSk3$H&q5ZAw6*RtTuUbL}UM_aU7CB&ugS&J2aPL_zwvM0_k#Y}XtL5fO^XT@~Z zewIr+O(|aw->~4i6JG!LvQ-#!Ahzx{1F_Pk9T2a-U+#w=i~C{*eA zieAjDLQvKD)zxPEQiXQC3UIn0VgU&TiATt)bn?YR7sS-ux>jdB(FIm;A5hS)7$+wr zhrF_^fvO!?5c!ju)vmgO4J`gqJ2q*txVrMJ<||}s^;Sqj_H%u|sdkyj)6OZP&LCFd z>WZTYfS__?s})5YSq7SRoN2~+;l6TD)tZsoJd_ z;nHYbFxHu_<;`YRV8psW4}5Oc;dNTS#?SgOoIx zHdVH)e$IYtRcS!2Jj06yjkK-`vMS0#hGV8xU(}dZ6|;t*rwu_;3t1KZwpt*5%{N)7 z`iU=gI11EDX$>MuGl*)b4e0^J>c==*`;!Uj1goYdck3r!XGm*(>Z9u5s1hgS-u0yqHt!HV zsL1v}-SM;%Q(AnlU9Wc+hND#L?r)tkv_XZo%XB<8w7vBE&xqi7S+1>1rd5o@mqiP>5^V~i-U;Jn@-OJ55X zwi|mSkg8h;fXCc>AHyup-mDz&-sc6$`p??f&>1Nm6DPno;%Zls$y^RQtBgj`8}7rp zX|f|)l@sbc@tiN?Yp^QM**DcuJEaUjfrB=s)U}F&)H%CHFywJBuf7k1re&7L@TR^h zgGABF2k<9l&N6mE5bQlPUgnA&J$3_6j&`9YK` z(t3;9$lZ#7nm~ko?z8l(sX)Xjb~0MQs5!Afp|%BLKnwuN#vEFc;!3m!WdKfBZuRgR zu+P&Bm>V}?7oI>d8B++FC7WusimZd=>6U{^Y)ncm(3#AF`BzKHvc;;WespZ%x)KQ|by$_qe52TBy3sxRdO7?+q`6*&dRaCTyXbHzK&Ey(B?H6xrfE>t-j zh>~#JH6e4(IWly-3a7`YD`M#pAZ9g9A2lt;ibqz^lLb&YW^eFdl_T;DX#zGtUpnH2 z6x-*?_%P0LU>a(9Tw+_fG)NsiH(zD%@tGZBX8nXM5i#cK6u61AQeG1IR$Vtymb5@D zOjG?hZLAMW;(U2?jtF1N9+5(fpDNKdf!mUu9*1-y1#iziT{63@XvB`>@GNn@@x+N? z{Yh3;pgW-%%E}M&O2pl`3jzn;UkU6I4XL(vB*s0_{vsqRaOa(RvP5d3M+SWi37f<* zLa2S9r+#n0WZD<0MDBV&ZTzxn9j2&P-}Wh#jwCu&mUGYEi&UGh)8Rb!(55GJy&@o~ z+z-oT{H|P9Q_n3{M$|156e+T|=g{f=yS^mrlARwBL>4LSXQcU%Y?{DySpoI~}$UyIN4@|yn3-lf(&Z~L%jJgx}2Ff2b6~X3i zkPl~y*duHES3CiQRvPg{uMlhR3B@%koSu)zuv1bR z+d_%WvazXS#8#}cuK|?dM_;Iw8^|4qi!8Z-R<6~2fJ>zS(K&Y29AQ`U8mZrF%xyCg zTzpz+jscbMwv9w^RT^LPL3E#qCgRV?>w*fH2n$$Yti#h9Rz4`8fJ^S+TR2#ULiw+uP}pZ?D&)?b$s#q>x)k|{=dU&^%a zoc1xGxz|gAGAWf<(8tFZ8;ytnE z2kNMhs2>D`NG|7AC_!?XVWP7xsF*HOs~`g*mCBzVl~| zOe{vDB38i$tWs~`j6xM***$~3?%Rk@PR(j4yBe9adpmavb?Bz2&D}x zdFU>fJOfohlrR^%RODAz_HN$AJbxzO%LGUB#z@+v2+RAMSD={M0P%%ZSCPv&<0q1& zc58ht`zfr%bI4K`lQNWclQp(!o@PdWA3QXsXvD$_$%So#?%B5ypK?>Mmu5$^U(ZLk zNJKYNoH!QHy)SVUkC+g1vD*&(ZSTf>%bLT&3`}C#4KqvVzpR5B(-BMX-eRSp6HOJhOSvHJc zvMVqb-O0Tm0s1DR^k_2I*S1nU=H%nh)(KW&0*-gTwAfLRSz#lGd!jwMNQAmjg2ufX z+Kd8b355e=svs@wwo02q*ep$qO?#CJk<6K-OjxzlgHZ%q9J(iOwPK$?kToBKrfZ!p zvY_W`wIr)@3&L-ZUO_dsVArfuIp??^GhfQ9RtPNhg2DSr${~lzT)>A zmcLc2vc;<(6*ov}wLy^LqugV?ZSg zd0B2beliSk4S98s&V?lEQ9!>U8Il0?do6R4n{O#AHOG<9yDS+egx*bu5I}>J(;Orl zeUMJM9ck6m^@R-qLn`7AK^L8T2xD=Oh;{X23(QIMQKr?(mezb^YIo?^GmEC5$FT}xj5Ft zV6J-lMab>Ay7-mfV=z-SAyYX#`zypW7?WxQo*h)yu9fP#p)AEt%e0c?>6@d|4;T8S z+ZXm7zLZ_9Xb>XkGD1V1ssiv#w<=$zUpjMaG4^=DK5ljX{}Lp6b}mZE3zr~PN&>YH zg)lUSTzgh22hKznj$r$+O6As_RE0u9L}Pfi(E7yzZ;dJ`M40X0(%--SlC(vY)q+I4bfWxi(~cM6r^c~7N#HbV69Use`~drt?Giw&l)K@ zo4v5~9_!?VFRWnQjP!{k@mA)n5SVyp)d1x^#D15{goi6l6y7TRJr=)Au~g;)bKFi09;6>J!C z3wqOQaes6nnwpoZ9aoZZt7{)q!p1>;oI3&1w^C3PsF%p(c3n}TV9|hd!|E!MN8Q)1 z7Z8nR)Tv+vQIMmX1N+FDXSt{lo(g`vJ>II_JlX^`;ZY@Zn}Wx910K^!E%AU70nMtIX4`9ZE8AXW9JhA!=XPqB0A348e5|h3kIdSbFOROMXgXk%uk4qi6qpOD_WMS z9NGAaEW8eSvOz)tkqAO^;qq1l`zDU%cnb6M#S>1+$Xp=;1gB^NXB%=1Z6r;=kwX>p zCcIvYsf5>OC~o=muVMn@(wAIwZo&i!@qtPS3^X?F3Obl?YbU)oA6>&y?r62^S=)5o zY8qsbsxDYvx#mNe*)Nl0U?x;aGF=Ji`LJC6>zb7v$DZ4|%HU4B9+mBpFsTg*03X=0Oav)@q5g z!p)<@r)znFHs<#@D5y-{f*9@F3Fkx2_VakR1y9;EkXz#dbR{AecM+|n7=oBAvo_Xp zb*^(v-W07&Zqe#>IWz=XRWE~vAAFZ-*@~x+%O83Disy@=z69l2XlpaYxkC!MN zwp$fD41ZL&t@U*v2u;fpBimjp_X-D9K{`Uqj=}v{l^%W6iA!1anh) zkdjifN0ID&ZxFe-w$0os-kXY~2M? zFQZgguDH0as5HXX${RG=1%&z*zX4fyLN4`+-ESNP* zme#J6S&A`-iuo{5FZ&iG2ecqDh^5_-E>~fJldG*Omw5IXBaU(h_2JG&lcI2PYgRll zd!-i3g4UJ!w5FgbvC}{~F4MK*m91;<{5Fs`u-Y-k7Ssc{1?31#S33Q%V8;DACO0<5 zf?3_u){cpw>z3Nkbqh#lD0*>7GP4ba=7LVX@I<-N{CaD|aVXqPPQk*;;rI1_xhIPphlmf+9|RD2 zuEZqYkyCgI62Gi03$?GHW>eY=TQCgslO)A7nK%`43qlC?W54Rz+}1d1J>4Oyq**v7 z`SeQKyPvXD&RAX_#?G2|hY@sY-C_|BZt839LL&tQkZ`YKV{Li&KKCbkA$UnRVC^ae z#ujFT+r@|`G~5BM7;p2vKKi9j@^Z}C35X@HP&w-{$Nh;}HUggWR0-1OG({=!YJ_Ij z1ZZ3!UnelPmR)63x5Zn*UKYfkE`~6%-pVET9V<066(KU3V>wo7GN&p(T>WmRPqdY{ zDcUO&l)Pgg=e?F@<+vxrMe6b!Q<_yDI)c);vK(EDoY5$e#|{a!wP!Du)}F`Qdvy+- z&S!$yE8NZVC5z+^8PN2OcI<(XF+5OdhM`3kboOX!yu8UICPl%h7!*=DOokEXC9CCm z)X&8|k)JH?x!}U{^sO3O<8Pb5M@B_|woX?P{=L$aMu|fDn;GMDm|A%j|JeW@8K7ko zRpJ(sP2egal#(A3^kOPmBcC~nPG}|g-hKS$x8JY7GVQ1SlJ+}nu>DDG&idh4i@}Zl zfA^=hrJetO{!`nfO8VzAwjU-Jy8ZkFpFfm*fBr`w5S)LB*XPea{^T{@6Mc4C>6yCy zIOk8WAA9_p_ujRwKc&u=d)_(%|H-Xye_DP1cKY8?B;E9nf8zWv{`rSr{pN=+fA~1R z|BL63K0UwEffHz4@;%{I&K!`|1z> z@a6w`=*M4Ve=|nF_9x_@JNeg!`}xn;whi{+4}WsxCnvT2*?FZ5>x4EY`?vp;`xnma zci+GJ!qNWnTYotCGu+vL>y}QJ=hOuLPp1B}Z@&5Vhwoqhwm%~O58r+H`>#HJ_pamp55IJ{fBgNoUw{0Qmp}gfZ~tT+@fW}T_FL!M n@y&gG@tfcOuaBLD!~On;FTeX?PDcvycz*XsfAklB^_Tw}g{fF; literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json new file mode 100644 index 0000000000..e12d3b5fd5 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Icon 16.pdf b/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Icon 16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9ab1864ed29b529ac0d88dc26866086ab3263f2e GIT binary patch literal 7715 zcma*sL602CaRuOe|BAjWAQ@0IGpj1I3J?TXyOv?thS!vDf)3Q|XhmZPoF;fZmfG*hecaCj`g_Sy}Oj#-bbItFF!orJzmz^zAb&}*Sg+%J+^hfzPl{9 zag?%7W9@s{>-B-L9eu6!T9@0g?c2Itms_bvJ4Q^a|JTA)+px{Gw%bwGt?$=6k1nb5dRw+yTDPBQ_uiL%uH(Vj z?N@JYIXoR>ulv}9{Wj`0I+s1Xv~`-cy6&T0tH3Nn=Eq zt@F7g_S;d{dfjjR=zBe8pvO`=%UasP66adqocB5sfL7Pjw{;n1XWNc#i$^!^92^?A zwlBRq;H`~C@UHzPlC5mhv8}C5PWo-_BDdKq&enD`Hiz5y{Te99ZQHM#u&u}F9ILHK z97{Rs!sncF8*3Zeb-k4~5}#}~_GRbna@(wu7?G;2&*Zjklp<0BHO78ev-GX^Vy|`^ zZR@S&x%F7s9@vhhN>SE1heHCxwk~z?p&aYF_k0z1MpAu2<)25B-X)+|*8L+JtjwTPfKy=)Q+V1YF_N6#E2hz0E4#8IQgwqH{kLy*Z#%^aG@4 zU9Ce^&MY<|jA>tcSMJNLx1)F$|A1rJOABDQImCv4=%cR(v<5|W0TU+h^-vyOg+)~5 zbU*M1j8=fLhy7OM()v<>xmC~3{m@lYzcG%w_!I_PrL`rSYO3YD_JaaY?7%+;ll{1& z$hR(2f^gJN{*`?eW-Ms{SHxq85ynopYe;+i+W+) zTdQH5Rq@wFI1W`Hm(vu8!Rv8T$>SstPA_H2*TK5lnDi!EG8|HtSKM(PPv%Nui4rXY zV9|$lbTIQJh$Z!!@0IWgffZxzsWMi0bLzPkgTOf30xWfYQZ4H-VN=C8kcOLm*67@` z?R5icRwx|Ha_;F!C3wLN`1+XitNur9;T{xXCv@bYym7s?HeiFF$4%rL3ShKMUTt}J z=0LA8?qK2*zD+D`ZJk^L$_}S%KTVJk5AM{fV8BtF;lLqbDdGijs=*kKT_}SD8!FXl z>VZ>-V#RZsoB|Pq6Iu&#Pp50zmi@L{l5nQ8cV}OL63Mmf$7RlY_-}ODyUU-#3=IUk zN08Hyw|9v`k<}4wljwVui<94qSvs)lyv={>gJIUzEMlRHR$+ zC2^pcTCUy!gnwV2Fz5c`-(4Or|Nj4%Hn3SnnZ54N#}Grb4J_%+i15n~r?$a==@gQn z3yGH40$ZIBV#J*lUBKjaCMzw4P&k-sB?Tr4ba9Z}G#4CHzSQoI8qL!8( zL6P=#G@zU-i{7(^_7tctl>$RE+7HtvSz#KACGNdwrLECEG0H$y+-a>EtZ5Ue1lMM z!UWJLk)!s~-AdC(vJO?Lx7d`P{n#KEo`Jy*8)ktH%fH*+bx1L$uypjO;sVaT8_wx@yvUt!@IiF3c)D*TI76KnDq;!!YbjFa zC18kX<{gAq7Ru~rn^B-L@Sr;raO=i&MwyD`*&+cYuIR7H`$t9AY_!R|>kj`<{|EtC zVsvA~tS?5jA+Rulc0+YjrZw4&$i_AE9H?U%p`T3}CQW5sAxn40y|?JP=`^>5JruVn z(p-w-wv5t+IwTz-@j0AW1~NLa8(9d|GI~_B*l{RtR@ILVu^}F}aWvbPPEkOBGKe!( z0Tt(^L<|ISOL47N%+8T|&hO)q=vgV=Bt$-uVOu;(r_E>2s|dnYa%d+SDK|l7LQI{qFdY^g`I!rev4HX@ z-i~wvQr52Ugj4?u#yw8alW=%dE2ZC4^AJpGh$ew&HZ(yx1CvuQx?dl>v0rLFLV(ee zMvyar?gu?otZL^RA*)=Nf+HO?Bc|C>OifTaqcSV45<~01(%)(Vf(B9dDF;bjdhO~#5-A_bWzkCX(f0ti>VF*Dk2{LM*@Wzfr@TtqkyD= zUm|ZXN~u#}y3B7Ue@r4ZK8;bQcZU-DJdtJ{EVve=b_H_D<(4UL$Z7vZw@nkyY+em! zUgsRzp&qPLiqO17sJwHOLw*F-ic4$3-`41YhJfy{kqzf82_@%tO)r~-@k#wamglBa z$n{pNQHbiyl?z^T)C_*ybfdG)`EvUbSPF^~NS@W52Q6}8!4|}Bh&3F6(N&UgNR8_; zxe5YCPT{=Fm$6k|^yLXNt-+=Qsw(!iO9Mb;k|Ls+t&v^|PuZ9vaV=Mq5Dr~EXGPwa z@|0Xt7AzQkp&BxDkbLWrc3jeMlNUJ4q-%Zxk&ILG3(RoU^Qb2rB5fA^2qY!!2{k%& zv1pcgYuTfGaW{)IHB;gckvUCnubCJ+>)9?)Z)DhVP=~pDe|gN$MpgMe@8!q3kH75Y zN4)bFzku)44}LKJ!w-S4{{Hdt>BIB&cfa?;puboD@xT9de|`1soA2*m4F7un-P=|9s6)kFQ_0`}*ne!~LT-BA8FcZ{NIU zyKXP0&wibZ>C>;4um1V%cU-;Z1h20j-}Cj;pFLZD;{P+tXFKUCpY4GxEStYncJbO-MR${Oj|bn%UuU zcPV7;eUL^q)75o3Rdu@c=KZ^GzVK5W20yxS`;UJP_Tqa8KFg+uA@4z&a;j8 zMRDmqq8CZ^-SbtPGAfC1m_n{TSJsi5?gAMJQ;I%RGuUNslGt0R85cq&X+ajWLglf)`0tj zn$QFLttjQ8pdya9lI4J2>B_}nsbszqhKj8&I>%aGl7XCXKQtdf73TQX027XthBowj z_GBmJFtyOLg4_FIxf*@_a3L{pLJ;c~DU5n5WXQbir4RHIN}fP>Haj;}dyt zOKmkm26E(Fb){Kth1Ltp!&GZ)VCQzAgs57@8CMf7w^3D9SS`O?B>fd7nKA~Dc;8&; zSRnM&noQPO73#r7CXE3*y~`0v23RHL!a7<)0Df9`XxT3)xHTZnyV^;HI!D4cqJ&4g z$hbre_QQZUOahsA!=8C1cj8Gv^5n>zr4Wl}NS5Mbm-B=zf-!0qKvA=4YT$SQsf)hO zGV(zuq=r(YdikmV9>A~{MvtY`=d}{BL_1DB1iGjau;#?ap_hRett-PM3fy^>BNdWxrbl!FXX#1W7&QArSrDKsjRDOEH& zHEp5`Gy#l`%mYP*v7H-Z<@70j{DkY$i}aIH~9 zuF<3B{p%m%+piAyGUSicDK1unI9SrTNhDv&-d`c*FJhvDe{);w_?@vol^9 z5`EclR5FxE!ySCn?7>4Js-@CCMKPHDeryw6xf|M|IIbf=RDs-LeneQ2rlALC)zf9i zN{kpgR0)j)o&(wKumK0bl8DchS)K|!t_qhO8dV`462kWwrYK>ZXcu~80_~B_0yE0A zb!ZA^D+ypnqX{EB#VJ?ltQzNqUPrG|t3UKy3`F}!*dmReOwt|=fwVOU9Yu!FHQ14e zW1}@N%Nv>llvC+FLZBM?K2WXM8k*I97c?KIUL)oZA{sF-f#gwf47OuPVxfSiAavJL zGv6Hiz|dV>f?3M5%s3#8grMRqT-wTjSVj7bJdaeu9GD2+DFlk7LR?83mYgmk_JJAj zp5fyuShS04G1|0-=?~{T74-AzU;|(t<_SndTm8~*AjC5Zgii|z^c^KE8~dRPz>2Bq z`A7yVblfO0dfca~JM(lt2b9K=RRUvScC_-m&KL{di_Ad_XsD18F_u|G^g^8b)Ur~X z10p*2DWD|%OcHXyNLg)IC99=->liZ!+E>YH!n$tt&?0*d&weGapPa%vY0-ogJ9$Pj z?lfl_hdMBzm)#Ce3bgR?EUEA<`q8>h)$Ay6n3t$EgIVlsmGF6BT!7hmoxOsg0$#et zteqH;a&?!T1$&ak&b@U+ie%B*OsTZ*Nfrx^{Zm+vVIi-V)4f)}6rWvuK6Wy7DEQ&Z4=VwXwmRM?D3q^Zz-yX|9J%%TiC} zN0k^g(6cBTZ<+gyDvqQa!i|M7*wn{}7iSRribGMLbBWs}8)77u4^z|`m?HsPMv|_l4vJ7G+Xf*LHs*T9Di~vQ3^DUKYRm?A+eR)pFeJCc zV9Zel0PD>8-K_*yE8fO$vpwnuW?Hy)9j@}fs9mH6GVYLF_pT#v28eSAYtRiU4qsf z+VCq0IVW(Mq)LdJllgw&@L?w8+y{XTcvfn#v20bBA{PU?$KX_+fGR|@^0d6wD1c!IKT=4gU;Os|e*5$|e)o4SsQF#|?Z5xpj2G{(K5iDlpEn zG=8U0&Ao1Y@UZe(^!&8bbh&xl{`hdcc^tW487~)gZ@2ePn|pS|`&Ib<>H*nE=uNbD zOD@rRdw%i9n-7>e%7e>sx6j+{kF51a_CF%o(*z*)G|9oob;D=J-(Edk-EKd=T>8_) z=KH~ob(nqCpCd;%=n~@PCurz5a)Mlby7Ja<6>-8i7LnP5ub2i0qT=>+Jw06A+-@EQ z{yX8~5AO)aXOG*v&FAB1Hy=KqgK}}X-9C+S*rI;%?aj{{thfJGxO#XpcbGVFc=P6W HfBEKr6W-}f diff --git a/DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/DownloadsPreferences.pdf b/DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/DownloadsPreferences.pdf similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/DownloadsPreferences.pdf rename to DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/DownloadsPreferences.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json new file mode 100644 index 0000000000..a17c70f1fe --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 10.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Icon 10.pdf b/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Icon 10.pdf new file mode 100644 index 0000000000000000000000000000000000000000..73ae44c94e97ff7b24e0784b029aa1d4e59a5c62 GIT binary patch literal 3482 zcmeHJOK%%D5We$Q@Dd;?5Q^^y5EzJ`DB2*d)kA=whxK}6E0wjbc3n96^_}5zNlJ22 z=UyIc>f7NwzL_EC)#d5=TfrP5f^ynleT)$;Lrj7HD+anK(zMBXFJr249rmB`KUK!;J|wVx-qjQHXV>Ni!k5lA0$!Gk@=gm=NCl+~VtpB4(jYbldBcx7gvCI!(? z$)xhe*&>w_Y?$-G2RqZjA%x_4T?Wht&Lcs}a}9n%9yN#hMPBy!Y{?I%F*}1zq`k8- zkDNiJ!va(Lo0vzzj91nuaM&5oVFQW8bQ$9VmCObOIf0O!QQQbXlI~n@8L1j2W7#PK zq)S%{hSM->su_}ydF`wKcjg|2OJ<5DTg2NY1z0yqMoUwOtgm2|-2yr$gK-)Xp)hvK z1(X29E~TKLL;`#QNZ?^OpbZs0Ce05ZfFguzzq~xeN+S^ zEuZvR<7JR^z5}D6(!%kuiwSMDMib@mVHVd=J`hXHeZ@5-H-k{L*p<+fkZ++J_gJ-i zTxSHcYrT~LbCaZU=N4557OVaSB9)ce8PJea0T=rg3ly)xT|ALV$fqza3Va@yyn&l|jI@{cksKs7!y zLQH)WbwQCQo$GS`Ijrcx!k?bMXVQ*oo6;_o%vm=X#ZfE`VR6d(1l^CBF2m5-(|EBQ zo3V@E&qVyU{Ll=4#@#f>;m@6^s|;tmyTt{?l1t;=?ze+q3z+NPuOIH3|5iQEZU;IL zNW*514XAJxO$@zg=vx41w*y%KwWmD{DbHjH6;4^wHk?IIH>>7H2UpZoW?(FfdGJ2zH)FQ}i^#wXUgCT`(vYBv zCxoU5(Ai&BH{dG82(IYt@k##(XT^d410aHYZ>$bCKZ_uU)%dUf+=qIhxD_rNdG7xT4PuD*B31&}+8 S&326Jz;GkTt5;_q&;J1ev)Ri4 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json new file mode 100644 index 0000000000..09604788db --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 11.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Icon 11.pdf b/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Icon 11.pdf new file mode 100644 index 0000000000000000000000000000000000000000..59f2624371964e36da805c6753e50bffc07732f8 GIT binary patch literal 4673 zcmeI0U5^~a6^8HoSJaIqh(vFFo~n`%64)V%k|@rSJH*8@9?YWgPO`H^gkPWMt?ua= z;|&t`j3ul0?W$Af^F62f#p_pJeiG(7O~HA0{Pu^*xlccJpM5s%AKvKCDPH3@f89U4 zefr)dz+0_4Jw5L4-c2{J_W!xx?Qj3|g?stc^22 zLvg!zlb=iMIiSOwQ%IF_=i1HJ=1#i@UcWCmI&FzQ@b7%s*y|(zgydqW@K9Rlz4H)~ zb)VdheKlG36)c7ls=31yQ6*XGF3chL#;F+*xy1z1di1`#GW%LW&Mu*1+J`uIXn^1fOE3N?g~$Ewl_UfE5V4OQ-79Zrrp$(R~aA6tW=lesvz zP*OoRq}Ak$u{F+MISzSfoA4j;OSxk+>pH@!9N*9^u`14%%?~H}*$S>?9g+=aDlRD* zF%r~!i+!d)@C)}1&Te3crZgtR_r{2%mIruGwy6{_AXpO3{SSf*p-$aGllg_(EaJ5>1 zE-v6E5-6@O&!+&b)v&uJ;#KhB8jCTtqLNz=*vx^97CkmZHMlKb*ad$kIn+g%)N2NXSc3D? zTtlxuyGHx5s+NJ;Mw^L138I58fpV|{(eBvVbIO560{g{ew1z&@fa=PUq0(J|da;gz ztje)=+6+-tR}8UnMx5c-(g)Xu9z%!z!RQD|8pv6l5E74b3axV*yJ>3lKq(Dc>80pE zj-+6IoD^OsgjWNHaWGA%)l9pHv!`3&*FIM|E)-3} zqixXCh)1ZNHb*0|KvQF5ojXcmkeW*>^sfV0^XQhr=#weN4a55j7NnzARkbCpBH|zJ zllsuCfa37IDs7GyMR%;?MmLsD!q$Yx>wj1j{H$(l+y4-#0B_xBZo5j*{O zu@S!>)WC}ojk6~zPQw0z3s-LuC8~(g!>*mmR7Hvs_Qm%-I}sNQLe7i`MIt0C0-COo zG(5Oe@tAsu3)+-0?0`NLapp-5XR3nCe91~HrBDkbleTuFR7U1A_|WCiEmKk?*piYX z5tsRDWHGtR{J>U9#S_8-GSM}nszD_(|jXrM5O-d~QI-oT&p;s?1-qwJ>rK>~Y;k#HN%VX^{3Pv$CyEoMRrOj`vZUv8>1_Ng@tuqdl(Ax5Q*+C*A_Ql6nD zQR0%6HfpJt22;aUm08UgZ={T;n0jS9R(Hh=$TBmZARk0;3j}oyJ9>d}#>uS1;V6xE zMIsyEFk3Mr8q{~- z2u+g4Xsno-RcaCl&Xn9tU2C=$luJh|w!$hccU1)WNoJu6XKA(^%mqDCwd7qTofl~- zB2R-fg~AU`U@JU17>)-@9PO0tjIv5uc;1Vxn3+k$IM`7UC07MP9gX5tYbNfTO5SHE zAGxETqzH3yBpGxnZ<(o)LXb%$g@odSqXIVL<{SeSq^x*hGXC>;Qic*1Wh~x>xV`+r zl>-_E2}vA-bJ>JqQW|DKw!%WVxe|Z{Du^OHfFY`p7HF98vXUVrN<*uJP`_kWDY^+Q z`)D&t)e^i$GCIli}p+|+E zae%fU&B#dx2-*Q-gVA5d!5#C+$V$-g7M7qt{f#Tr@IDk|9Hwe2WD)Cfw$)3G>PP2i z7~yJm(rtge%AfE3y5cY219ZXMJ_pd%E}sc*K7V*PKAqgx-|*Rh-_1Y&`>(ybd42bG zzXE^Tzq!Br;`o#Mdh`*EU6+TzMRxq$<>@Txc7Hnl__*7joIXt4uHs%EAD;FPoJhOf zz}I(=V2(aLpsQ~eg0^oOH-EW*gQ`vz+`98QFONU6H+-P~3FJx=?e#6{s35Y6%f zNcY)wi*|F+K-Pt TcaKlQ97-ZgFJAod?_d58P79)> literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index b43155f629..c43f7dc27c 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -41,7 +41,7 @@ final class AutoconsentUserScript: NSObject, WKScriptMessageHandlerWithReply, Us private weak var selfTestFrameInfo: WKFrameInfo? private var topUrl: URL? - private let preferences = PrivacySecurityPreferences.shared + private let preferences = CookiePopupProtectionPreferences.shared private let management = AutoconsentManagement.shared public var messageNames: [String] { MessageName.allCases.map(\.rawValue) } @@ -209,7 +209,7 @@ extension AutoconsentUserScript { return } - if preferences.autoconsentEnabled == false { + if preferences.isAutoconsentEnabled == false { // this will only happen if the user has just declined a prompt in this tab replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection return @@ -240,7 +240,7 @@ extension AutoconsentUserScript { "rules": nil, // rules are bundled with the content script atm "config": [ "enabled": true, - "autoAction": preferences.autoconsentEnabled == true ? "optOut" : nil, + "autoAction": preferences.isAutoconsentEnabled == true ? "optOut" : nil, "disabledCmps": disabledCMPs, "enablePrehide": true, "enableCosmeticRules": true, diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 93b95b830c..bbc73f1f3a 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -343,7 +343,8 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { } public func secureVaultManager(_: SecureVaultManager, didRequestRuntimeConfigurationForDomain domain: String, completionHandler: @escaping (String?) -> Void) { - let properties = ContentScopeProperties(gpcEnabled: PrivacySecurityPreferences.shared.gpcEnabled, + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled + let properties = ContentScopeProperties(gpcEnabled: isGPCEnabled, sessionKey: topAutofillUserScript?.sessionKey ?? "", featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfigurationManager.privacyConfig)) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 1e367e4e6c..3f5b12e8f7 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -340,13 +340,21 @@ extension URL { } static var cookieConsentPopUpManagement: URL { - return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#cookie-consent-pop-up-management")! + return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#cookie-pop-up-management")! } static var gpcLearnMore: URL { return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/gpc/")! } + static var privateSearchLearnMore: URL { + return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/search-privacy/")! + } + + static var searchSettings: URL { + return URL(string: "https://duckduckgo.com/settings/")! + } + static var ddgLearnMore: URL { return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/get-duckduckgo/get-duckduckgo-browser-on-mac/")! } @@ -362,6 +370,7 @@ extension URL { static var duckDuckGoEmail = URL(string: "https://duckduckgo.com/email-protection")! static var duckDuckGoEmailLogin = URL(string: "https://duckduckgo.com/email")! + static var duckDuckGoEmailInfo = URL(string: "https://duckduckgo.com/duckduckgo-help-pages/email-protection/what-is-duckduckgo-email-protection/")! static var duckDuckGoMorePrivacyInfo = URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/atb/")! var isDuckDuckGo: Bool { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 3c142fd364..66e81e8608 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -329,10 +329,12 @@ struct UserText { value: "Fireproofing this site will keep you signed in after using the Fire Button.", comment: "Fireproof confirmation message") static let webTrackingProtectionSettingsTitle = NSLocalizedString("web.tracking.protection.title", value: "Web Tracking Protection", comment: "Web tracking protection settings section title") - static let webTrackingProtectionExplenation = NSLocalizedString("web.tracking.protection.explenation", value: "DuckDuckGo automatically blocks hidden trackers as you browse the web.", comment: "feature explanation in settings") - static let autoconsentSettingsTitle = NSLocalizedString("autoconsent.title", value: "Cookie Pop-ups", comment: "Autoconsent settings section title") + static let webTrackingProtectionExplenation = NSLocalizedString("web.tracking.protection.explenation", value: "DuckDuckGo automatically blocks hidden trackers as you browse the web.", comment: "Privacy feature explanation in the browser settings") static let autoconsentCheckboxTitle = NSLocalizedString("autoconsent.checkbox.title", value: "Automatically handle cookie pop-ups", comment: "Autoconsent settings checkbox title") static let autoconsentExplanation = NSLocalizedString("autoconsent.explanation", value: "DuckDuckGo will try to select the most private settings available and hide these pop-ups for you.", comment: "Autoconsent feature explanation in settings") + static let privateSearchExplanation = NSLocalizedString("private.search.explenation", value: "DuckDuckGo Private Search is your default search engine, so you can search the web without being tracked.", comment: "feature explanation in settings") + static let webTrackingProtectionExplanation = NSLocalizedString("web.tracking.protection.explanation", value: "DuckDuckGo automatically blocks hidden trackers as you browse the web.", comment: "feature explanation in settings") + static let emailProtectionExplanation = NSLocalizedString("email.protection.explanation", value: "Block email trackers and hide your address without switching your email provider.", comment: "Email protection feature explanation in settings. The feature blocks email trackers and hides original email address.") // Misc @@ -342,7 +344,6 @@ struct UserText { static let duckPlayerOff = NSLocalizedString("duck-player.off", value: "Never use Duck Player", comment: "Private YouTube Player option") static let duckPlayerExplanation = NSLocalizedString("duck-player.explanation", value: "Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations.", comment: "Private YouTube Player explanation in settings") - static let gpcSettingsTitle = NSLocalizedString("gpc.title", value: "Global Privacy Control (GPC)", comment: "GPC settings title") static let gpcCheckboxTitle = NSLocalizedString("gpc.checkbox.title", value: "Enable Global Privacy Control", comment: "GPC settings checkbox title") static let gpcExplanation = NSLocalizedString("gpc.explanation", value: "Tells participating websites not to sell or share your data.", comment: "GPC explanation in settings") static let learnMore = NSLocalizedString("learnmore.link", value: "Learn More", comment: "Learn More link") @@ -516,8 +517,8 @@ struct UserText { static let settings = NSLocalizedString("settings", value: "Settings", comment: "Menu item for opening settings") - static let general = NSLocalizedString("preferences.general", value: "General", comment: "Show general preferences") - static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Show sync preferences") + static let general = NSLocalizedString("preferences.general", value: "General", comment: "Title of the option to show the General preferences") + static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Title of the option to show the Sync preferences") static let syncAutoLockPrompt = NSLocalizedString("preferences.sync.auto-lock-prompt", value:"Unlock device to setup Sync & Backup", comment: "Reason for auth when setting up Sync") static let syncBookmarkPausedAlertTitle = NSLocalizedString("alert.sync-bookmarks-paused-title", value: "Bookmarks Sync is Paused", comment: "Title for alert shown when sync bookmarks paused for too many items") static let syncBookmarkPausedAlertDescription = NSLocalizedString("alert.sync-bookmarks-paused-description", value: "You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up.", comment: "Description for alert shown when sync bookmarks paused for too many items") @@ -526,14 +527,29 @@ struct UserText { static let syncPausedTitle = NSLocalizedString("alert.sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") static let syncUnavailableMessage = NSLocalizedString("alert.sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("alert.sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") - static let defaultBrowser = NSLocalizedString("preferences.default-browser", value: "Default Browser", comment: "Show default browser preferences") - static let appearance = NSLocalizedString("preferences.appearance", value: "Appearance", comment: "Show appearance preferences") - static let privacy = NSLocalizedString("preferences.privacy", value: "Privacy", comment: "Show privacy browser preferences") - static let vpn = NSLocalizedString("preferences.vpn", value: "VPN", comment: "Show VPN preferences") - static let duckPlayer = NSLocalizedString("preferences.duck-player", value: "Duck Player", comment: "Show Duck Player browser preferences") - static let about = NSLocalizedString("preferences.about", value: "About", comment: "Show about screen") - - static let downloads = NSLocalizedString("preferences.downloads", value: "Downloads", comment: "Show downloads browser preferences") + static let privacyProtections = NSLocalizedString("preferences.privacy-protections", value: "Privacy Protections", comment: "The section header in Preferences representing browser features related to privacy protection") + static let mainSettings = NSLocalizedString("preferences.main-settings", value: "Main Settings", comment: "Section header in Preferences for main settings") + static let preferencesOn = NSLocalizedString("preferences.on", value: "On", comment: "Status indicator of a browser privacy protection feature.") + static let preferencesOff = NSLocalizedString("preferences.off", value: "Off", comment: "Status indicator of a browser privacy protection feature.") + static let preferencesAlwaysOn = NSLocalizedString("preferences.always-on", value: "Always On", comment: "Status indicator of a browser privacy protection feature.") + static let duckduckgoOnOtherPlatforms = NSLocalizedString("preferences.duckduckgo-on-other-platforms", value: "DuckDuckGo on Other Platforms", comment: "Button presented to users to navigate them to our product page which presents all other products for other platforms") + static let defaultBrowser = NSLocalizedString("preferences.default-browser", value: "Default Browser", comment: "Title of the option to show the Default Browser Preferences") + static let privateSearch = NSLocalizedString("preferences.private-search", value: "Private Search", comment: "Title of the option to show the Private Search preferences") + static let appearance = NSLocalizedString("preferences.appearance", value: "Appearance", comment: "Title of the option to show the Appearance preferences") + static let dataClearing = NSLocalizedString("preferences.data-clearing", value: "Data Clearing", comment: "Title of the option to show the Data Clearing preferences") + static let webTrackingProtection = NSLocalizedString("preferences.web-tracking-protection", value: "Web Tracking Protection", comment: "Title of the option to show the Web Tracking Protection preferences") + static let emailProtectionPreferences = NSLocalizedString("preferences.email-protection", value: "Email Protection", comment: "Title of the option to show the Email Protection preferences") + static let autofillEnabledFor = NSLocalizedString("preferences.autofill-enabled-for", value: "Autofill enabled in this browser for:", comment: "Label presented before the email account in email protection preferences") + + static let vpn = NSLocalizedString("preferences.vpn", value: "VPN", comment: "Title of the option to show the VPN preferences") + static let duckPlayer = NSLocalizedString("preferences.duck-player", value: "Duck Player", comment: "Title of the option to show the Duck Player browser preferences") + static let about = NSLocalizedString("preferences.about", value: "About", comment: "Title of the option to show the About screen") + + static let accessibility = NSLocalizedString("preferences.accessibility", value: "Accessibility", comment: "Title of the option to show the Accessibility browser preferences") + static let cookiePopUpProtection = NSLocalizedString("preferences.cookie-pop-up-protection", value: "Cookie Pop-Up Protection", comment: "Title of the option to show the Cookie Pop-Up Protection preferences") + static let downloads = NSLocalizedString("preferences.downloads", value: "Downloads", comment: "Title of the downloads browser preferences") + static let support = NSLocalizedString("preferences.support", value: "Support", comment: "Open support page") + static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") @@ -568,10 +584,9 @@ struct UserText { static let addressBar = NSLocalizedString("preferences.appearance.address-bar", value: "Address Bar", comment: "Theme preferences") static let showFullWebsiteAddress = NSLocalizedString("preferences.appearance.show-full-url", value: "Full website address", comment: "Option to show full URL in the address bar") static let showAutocompleteSuggestions = NSLocalizedString("preferences.appearance.show-autocomplete-suggestions", value: "Autocomplete suggestions", comment: "Option to show autocomplete suggestions in the address bar") - static let zoomSettingTitle = NSLocalizedString("preferences.appearance.zoom", value: "Zoom", comment: "Zoom settings section title") static let zoomPickerTitle = NSLocalizedString("preferences.appearance.zoom-picker", value: "Default page zoom", comment: "Default page zoom picker title") static let defaultZoomPageMoreOptionsItem = NSLocalizedString("more-options.zoom.default-zoom-page", value: "Change Default Page Zoom…", comment: "Default page zoom picker title") - static let autofill = NSLocalizedString("preferences.autofill", value: "Autofill", comment: "Show Autofill preferences") + static let autofill = NSLocalizedString("preferences.autofill", value: "Passwords", comment: "Show Autofill preferences") static let aboutDuckDuckGo = NSLocalizedString("preferences.about.about-duckduckgo", value: "About DuckDuckGo", comment: "About screen") static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") diff --git a/DuckDuckGo/ContentBlocker/ContentBlocking.swift b/DuckDuckGo/ContentBlocker/ContentBlocking.swift index dee72a7514..795f5ec581 100644 --- a/DuckDuckGo/ContentBlocker/ContentBlocking.swift +++ b/DuckDuckGo/ContentBlocker/ContentBlocking.swift @@ -86,7 +86,7 @@ final class AppContentBlocking { privacyConfigurationManager: privacyConfigurationManager, trackerDataManager: trackerDataManager, configStorage: configStorage, - privacySecurityPreferences: PrivacySecurityPreferences.shared, + webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, tld: tld) adClickAttributionRulesProvider = AdClickAttributionRulesProvider(config: adClickAttribution, diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index 4013498fff..979ecaf469 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -37,7 +37,7 @@ protocol ScriptSourceProviding { // refactor: ScriptSourceProvider to be passed to init methods as `some ScriptSourceProviding`, DefaultScriptSourceProvider to be killed // swiftlint:disable:next identifier_name func DefaultScriptSourceProvider() -> ScriptSourceProviding { - ScriptSourceProvider(configStorage: ConfigurationStore.shared, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, privacySettings: PrivacySecurityPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) + ScriptSourceProvider(configStorage: ConfigurationStore.shared, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) } struct ScriptSourceProvider: ScriptSourceProviding { @@ -52,19 +52,19 @@ struct ScriptSourceProvider: ScriptSourceProviding { let privacyConfigurationManager: PrivacyConfigurationManaging let contentBlockingManager: ContentBlockerRulesManagerProtocol let trackerDataManager: TrackerDataManager - let privacySettings: PrivacySecurityPreferences + let webTrakcingProtectionPreferences: WebTrackingProtectionPreferences let tld: TLD init(configStorage: ConfigurationStoring, privacyConfigurationManager: PrivacyConfigurationManaging, - privacySettings: PrivacySecurityPreferences, + webTrackingProtectionPreferences: WebTrackingProtectionPreferences, contentBlockingManager: ContentBlockerRulesManagerProtocol, trackerDataManager: TrackerDataManager, tld: TLD) { self.configStorage = configStorage self.privacyConfigurationManager = privacyConfigurationManager - self.privacySettings = privacySettings + self.webTrakcingProtectionPreferences = webTrackingProtectionPreferences self.contentBlockingManager = contentBlockingManager self.trackerDataManager = trackerDataManager self.tld = tld @@ -83,7 +83,7 @@ struct ScriptSourceProvider: ScriptSourceProviding { public func buildAutofillSource() -> AutofillUserScriptSourceProvider { let privacyConfig = self.privacyConfigurationManager.privacyConfig return DefaultAutofillSourceProvider.Builder(privacyConfigurationManager: privacyConfigurationManager, - properties: ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + properties: ContentScopeProperties(gpcEnabled: webTrakcingProtectionPreferences.isGPCEnabled, sessionKey: self.sessionKey ?? "", featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig)), isDebug: AutofillPreferences().debugScriptEnabled) diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 9313c0530a..f3b2cf95d0 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -46,9 +46,9 @@ final class DBPHomeViewController: NSViewController { inlineIconCredentials: false, thirdPartyCredentialsProvider: false) - let privacySettings = PrivacySecurityPreferences.shared + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let sessionKey = UUID().uuidString - let prefs = ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + let prefs = ContentScopeProperties(gpcEnabled: isGPCEnabled, sessionKey: sessionKey, featureToggles: features) diff --git a/DuckDuckGo/Email/EmailUrlExtensions.swift b/DuckDuckGo/Email/EmailUrlExtensions.swift index 0055748b94..6b86f032c6 100644 --- a/DuckDuckGo/Email/EmailUrlExtensions.swift +++ b/DuckDuckGo/Email/EmailUrlExtensions.swift @@ -25,6 +25,7 @@ extension EmailUrls { static let emailProtectionLink = "https://duckduckgo.com/email" static let emailProtectionInContextSignupLink = "https://duckduckgo.com/email/start-incontext" static let emailProtectionAccountLink = "https://duckduckgo.com/email/settings/account" + static let emailProtectionSupportLink = "https://duckduckgo.com/email/settings/support" } var emailProtectionLink: URL { @@ -39,6 +40,10 @@ extension EmailUrls { return URL(string: Url.emailProtectionAccountLink)! } + var emailProtectionSupportLink: URL { + return URL(string: Url.emailProtectionSupportLink)! + } + func isDuckDuckGoEmailProtection(url: URL) -> Bool { return url.absoluteString.starts(with: Url.emailProtectionLink) } diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index cd9f965ea9..6fb1869064 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -71,7 +71,6 @@ extension HomePage.Models { private let dataImportProvider: DataImportStatusProviding private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager - private let privacyPreferences: PrivacySecurityPreferences private let duckPlayerPreferences: DuckPlayerPreferencesPersistor private let randomNumberGenerator: RandomNumberGenerating @@ -144,7 +143,6 @@ extension HomePage.Models { dataImportProvider: DataImportStatusProviding, tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), - privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, duckPlayerPreferences: DuckPlayerPreferencesPersistor, homePageRemoteMessaging: HomePageRemoteMessaging, privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, @@ -153,7 +151,6 @@ extension HomePage.Models { self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager - self.privacyPreferences = privacyPreferences self.duckPlayerPreferences = duckPlayerPreferences self.homePageRemoteMessaging = homePageRemoteMessaging self.privacyConfigurationManager = privacyConfigurationManager @@ -170,6 +167,7 @@ extension HomePage.Models { switch featureType { case .defaultBrowser: do { + Pixel.fire(.defaultRequestedFromHomepageSetupView) try defaultBrowserProvider.presentDefaultBrowserPrompt() } catch { defaultBrowserProvider.openSystemPreferences() diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 03f2cb4e08..96fb2aca47 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -42,7 +42,9 @@ final class HomePageViewController: NSViewController { var defaultBrowserModel: HomePage.Models.DefaultBrowserModel! var recentlyVisitedModel: HomePage.Models.RecentlyVisitedModel! var featuresModel: HomePage.Models.ContinueSetUpModel! - var appearancePreferences: AppearancePreferences! + let accessibilityPreferences: AccessibilityPreferences + let appearancePreferences: AppearancePreferences + let defaultBrowserPreferences: DefaultBrowserPreferences var cancellables = Set() @UserDefaultsWrapper(key: .defaultBrowserDismissed, defaultValue: false) @@ -56,13 +58,19 @@ final class HomePageViewController: NSViewController { bookmarkManager: BookmarkManager, historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, fireViewModel: FireViewModel? = nil, - onboardingViewModel: OnboardingViewModel = OnboardingViewModel()) { + onboardingViewModel: OnboardingViewModel = OnboardingViewModel(), + accessibilityPreferences: AccessibilityPreferences = AccessibilityPreferences.shared, + appearancePreferences: AppearancePreferences = AppearancePreferences.shared, + defaultBrowserPreferences: DefaultBrowserPreferences = DefaultBrowserPreferences.shared) { self.tabCollectionViewModel = tabCollectionViewModel self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating self.fireViewModel = fireViewModel ?? FireCoordinator.fireViewModel self.onboardingViewModel = onboardingViewModel + self.accessibilityPreferences = accessibilityPreferences + self.appearancePreferences = appearancePreferences + self.defaultBrowserPreferences = defaultBrowserPreferences super.init(nibName: nil, bundle: nil) } @@ -72,7 +80,6 @@ final class HomePageViewController: NSViewController { defaultBrowserModel = createDefaultBrowserModel() recentlyVisitedModel = createRecentlyVisitedModel() featuresModel = createFeatureModel() - appearancePreferences = AppearancePreferences.shared refreshModels() @@ -81,6 +88,7 @@ final class HomePageViewController: NSViewController { .environmentObject(defaultBrowserModel) .environmentObject(recentlyVisitedModel) .environmentObject(featuresModel) + .environmentObject(accessibilityPreferences) .environmentObject(appearancePreferences) .onTapGesture { [weak self] in // Remove focus from the address bar if interacting with this view. @@ -149,8 +157,9 @@ final class HomePageViewController: NSViewController { } func createDefaultBrowserModel() -> HomePage.Models.DefaultBrowserModel { - return .init(isDefault: DefaultBrowserPreferences().isDefault, wasClosed: defaultBrowserDismissed, requestSetDefault: { [weak self] in - let defaultBrowserPreferencesModel = DefaultBrowserPreferences() + return .init(isDefault: DefaultBrowserPreferences.shared.isDefault, wasClosed: defaultBrowserDismissed, requestSetDefault: { [weak self] in + Pixel.fire(.defaultRequestedFromHomepage) + let defaultBrowserPreferencesModel = DefaultBrowserPreferences.shared defaultBrowserPreferencesModel.becomeDefault { [weak self] isDefault in _ = defaultBrowserPreferencesModel self?.defaultBrowserModel.isDefault = isDefault @@ -196,7 +205,7 @@ final class HomePageViewController: NSViewController { } func refreshDefaultBrowserModel() { - let prefs = DefaultBrowserPreferences() + let prefs = DefaultBrowserPreferences.shared if prefs.isDefault { defaultBrowserDismissed = false } diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index cce98e8330..e4f7972ce7 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -7,7 +7,7 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, @@ -19,43 +19,43 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "it" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 94ea695d1b..ce218cd1b2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -53,8 +53,57 @@ } } }, - "-1 -> 0 ... 1" : { - + " %@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + } + } }, "**%lld** tracking attempts blocked" : { "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@", @@ -430,24 +479,6 @@ } } } - }, - "0->1->nil" : { - - }, - "10%" : { - - }, - "20%" : { - - }, - "50%" : { - - }, - "80%" : { - - }, - "100%" : { - }, "about.app_name" : { "comment" : "Application name to be displayed in the About dialog", @@ -1966,9 +1997,6 @@ } } } - }, - "Animate" : { - }, "auth.alert.login.button" : { "comment" : "Authentication Alert Sign In Button", @@ -2872,7 +2900,7 @@ }, "autoconsent.title" : { "comment" : "Autoconsent settings section title", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15926,6 +15954,66 @@ } } }, + "email.protection.explanation" : { + "comment" : "Email protection feature explanation in settings. The feature blocks email trackers and hides original email address.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "E-Mail-Tracker blockieren und deine Adresse verbergen, ohne den E-Mail-Anbieter zu wechseln." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Block email trackers and hide your address without switching your email provider." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquea los rastreadores de correo electrónico y oculta tu dirección sin cambiar de proveedor de correo electrónico." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquez les traqueurs d'e-mails et masquez votre adresse sans changer de fournisseur de messagerie." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blocca i sistemi di tracciamento delle e-mail e nascondi il tuo indirizzo, senza cambiare il provider di posta elettronica." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokkeer e-mailtrackers en verberg je adres zonder van e-mailprovider te wisselen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zablokuj mechanizmy śledzące pocztę e-mail i ukryj swój adres bez zmiany dostawcy poczty e-mail." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloqueia os rastreadores de e-mail e oculta o teu endereço sem alterares o teu fornecedor de e-mail." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чтобы блокировать трекеры и скрывать свой адрес, вовсе не нужно менять почтовый сервис." + } + } + } + }, "Enter Full Screen" : { "comment" : "Main Menu View item", "localizations" : { @@ -19567,7 +19655,7 @@ }, "gpc.title" : { "comment" : "GPC settings title", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -24174,9 +24262,6 @@ } } } - }, - "Indeterminate (-1)" : { - }, "invite.dialog.get.started.button" : { "comment" : "Get Started button on an invite dialog", @@ -29790,9 +29875,6 @@ } } } - }, - "nil->0->nil" : { - }, "no.access.to.downloads.folder.header" : { "comment" : "Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder", @@ -31678,7 +31760,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Apri collegamento in una nuova scheda" + "value" : "Apri link in una nuova scheda" } }, "nl" : { @@ -41693,7 +41775,7 @@ } }, "preferences.about" : { - "comment" : "Show about screen", + "comment" : "Title of the option to show the About screen", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -42172,8 +42254,128 @@ } } }, + "preferences.accessibility" : { + "comment" : "Title of the option to show the Accessibility browser preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barrierefreiheit" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Accessibility" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesibilidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accessibilité" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accessibilità" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegankelijkheid" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dostępność" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acessibilidade" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Универсальный доступ" + } + } + } + }, + "preferences.always-on" : { + "comment" : "Status indicator of a browser privacy protection feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immer aktiviert" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Always On" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours activé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sempre attiva" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altijd aan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawsze włączone" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sempre ligada" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всегда включено" + } + } + } + }, "preferences.appearance" : { - "comment" : "Show appearance preferences", + "comment" : "Title of the option to show the Appearance preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -42654,7 +42856,7 @@ }, "preferences.appearance.zoom" : { "comment" : "Zoom settings section title", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -42779,61 +42981,241 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Autovervollständigen" + "value" : "Passwörter" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Autofill" + "value" : "Passwords" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Autocompletar" + "value" : "Contraseñas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Saisie automatique" + "value" : "Mots de passe" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Compilazione automatica" + "value" : "Password" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Automatisch invullen" + "value" : "Wachtwoorden" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Autouzupełnianie" + "value" : "Hasła" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Preenchimento automático" + "value" : "Palavras-passe" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Автозаполнение" + "value" : "Пароли" + } + } + } + }, + "preferences.autofill-enabled-for" : { + "comment" : "Label presented before the email account in email protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autovervollständigen in diesem Browser aktiviert für:" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Autofill enabled in this browser for:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autocompletar habilitado en este navegador para:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La saisie automatique est activée dans ce navigateur pour :" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compilazione automatica abilitata in questo browser per:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "'Automatisch aanvullen' is ingeschakeld in deze browser voor:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autouzupełnianie włączone w tej przeglądarce dla:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preenchimento automático ativado neste navegador para:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В этом браузере включено автозаполнение для:" + } + } + } + }, + "preferences.cookie-pop-up-protection" : { + "comment" : "Title of the option to show the Cookie Pop-Up Protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cookie-Pop-up-Schutz" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cookie Pop-Up Protection" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protección contra ventanas emergentes de cookies" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection contre les fenêtres contextuelles des cookies" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezione pop-up dei cookie" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bescherming tegen cookiepop-ups" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona przed wyskakującymi okienkami dotyczącymi plików cookie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteção contra pop-ups de cookies" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита от всплывающих окон куки" + } + } + } + }, + "preferences.data-clearing" : { + "comment" : "Title of the option to show the Data Clearing preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datenlöschung" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Data Clearing" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminación de datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacement des données" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellazione dati" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gegevens wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czyszczenie danych" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpeza de Dados" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистка данных" } } } }, "preferences.default-browser" : { - "comment" : "Show default browser preferences", + "comment" : "Title of the option to show the Default Browser Preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43073,7 +43455,7 @@ } }, "preferences.downloads" : { - "comment" : "Show downloads browser preferences", + "comment" : "Title of the downloads browser preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43133,7 +43515,7 @@ } }, "preferences.duck-player" : { - "comment" : "Show Duck Player browser preferences", + "comment" : "Title of the option to show the Duck Player browser preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43192,8 +43574,128 @@ } } }, + "preferences.duckduckgo-on-other-platforms" : { + "comment" : "Button presented to users to navigate them to our product page which presents all other products for other platforms", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo auf anderen Plattformen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo on Other Platforms" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo en otras plataformas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo sur d'autres plateformes" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo su altre piattaforme" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo op andere platforms" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo na innych platformach" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Noutras Plataformas" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo для других платформ" + } + } + } + }, + "preferences.email-protection" : { + "comment" : "Title of the option to show the Email Protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "E-Mail-Schutz" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Email Protection" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protección del correo electrónico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection des e-mails" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezione email" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "E-mailbescherming" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona poczty e-mail" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteção de e-mail" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита электронной почты" + } + } + } + }, "preferences.general" : { - "comment" : "Show general preferences", + "comment" : "Title of the option to show the General preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43252,6 +43754,186 @@ } } }, + "preferences.main-settings" : { + "comment" : "Section header in Preferences for main settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haupteinstellungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Main Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajustes principales" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réglages principaux" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impostazioni principali" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoofdinstellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia główne" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definições Principais" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Основные настройки" + } + } + } + }, + "preferences.off" : { + "comment" : "Status indicator of a browser privacy protection feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aus" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Off" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desactivado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Désactivé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disattivato" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uit" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wył." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desligado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выкл." + } + } + } + }, + "preferences.on" : { + "comment" : "Status indicator of a browser privacy protection feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "An" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "On" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "On" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wł." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вкл." + } + } + } + }, "preferences.on-startup" : { "comment" : "Name of the preferences section related to app startup", "extractionState" : "extracted_with_value", @@ -43314,7 +43996,7 @@ }, "preferences.privacy" : { "comment" : "Show privacy browser preferences", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -43372,6 +44054,126 @@ } } }, + "preferences.privacy-protections" : { + "comment" : "The section header in Preferences representing browser features related to privacy protection", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datenschutz" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Privacy Protections" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protecciones de privacidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protections de la confidentialité" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezioni della Privacy" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacybescherming" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mechanizmy Ochrony Prywatności" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteções de Privacidade" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита конфиденциальности" + } + } + } + }, + "preferences.private-search" : { + "comment" : "Title of the option to show the Private Search preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Private Suche" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Private Search" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Búsqueda privada" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recherche privée" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricerca Privata" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privézoekopdracht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prywatne wyszukiwanie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pesquisa Privada" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Частный поиск" + } + } + } + }, "preferences.reopen-windows" : { "comment" : "Option to control session restoration", "extractionState" : "extracted_with_value", @@ -43492,8 +44294,68 @@ } } }, + "preferences.support" : { + "comment" : "Open support page", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterstützung" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Support" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aide" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supporto" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Support" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wsparcie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Assistência" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поддержка" + } + } + } + }, "preferences.sync" : { - "comment" : "Show sync preferences", + "comment" : "Title of the option to show the Sync preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43613,7 +44475,7 @@ } }, "preferences.vpn" : { - "comment" : "Show VPN preferences", + "comment" : "Title of the option to show the VPN preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43672,6 +44534,66 @@ } } }, + "preferences.web-tracking-protection" : { + "comment" : "Title of the option to show the Web Tracking Protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Web Tracking Protection" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Web Tracking Protection" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protección de rastreo en la web" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection contre le pistage sur le Web" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezione dal tracciamento web" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bescherming tegen webtracking" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona przed śledzeniem w sieci" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteção contra rastreamento na internet" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита от отслеживания онлайн" + } + } + } + }, "print.menu.item" : { "comment" : "Menu item title", "extractionState" : "extracted_with_value", @@ -43732,6 +44654,66 @@ } } }, + "private.search.explenation" : { + "comment" : "feature explanation in settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search ist deine Standardsuchmaschine – du kannst also das Web durchsuchen, ohne getrackt zu werden." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo Private Search is your default search engine, so you can search the web without being tracked." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La búsqueda privada de DuckDuckGo es tu motor de búsqueda predeterminado para que puedas buscar en la web sin que te rastreen." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search est votre moteur de recherche par défaut, qui vous permet d'effectuer des recherches sur le Web sans être suivi(e)." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search è il tuo motore di ricerca predefinito, quindi puoi effettuare ricerche sul Web senza essere tracciato." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search is je standaardzoekmachine, zodat je op het web kunt zoeken zonder gevolgd te worden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search to domyślna wyszukiwarka, dzięki której możesz wyszukiwać treści w sieci, unikając śledzenia." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo Private Search é o teu motor de pesquisa predefinido, para que possas pesquisar a web sem seres rastreado." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search — ваша поисковая система по умолчанию. Вы можете пользоваться поиском в интернете, не опасаясь слежки." + } + } + } + }, "quit" : { "comment" : "Quit button", "extractionState" : "extracted_with_value", @@ -44363,9 +45345,6 @@ } } } - }, - "Reset (nil)" : { - }, "restart.bitwarden" : { "comment" : "Button to restart Bitwarden application", @@ -46473,9 +47452,6 @@ } } } - }, - "Slow animations" : { - }, "Smart Copy/Paste" : { "comment" : "Main Menu Edit-Substitutions item", @@ -49937,7 +50913,7 @@ } } }, - "web.tracking.protection.explenation" : { + "web.tracking.protection.explanation" : { "comment" : "feature explanation in settings", "extractionState" : "extracted_with_value", "localizations" : { @@ -49997,6 +50973,66 @@ } } }, + "web.tracking.protection.explenation" : { + "comment" : "Privacy feature explanation in the browser settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo blockiert beim Durchsuchen des Internets automatisch ausgeblendete Tracker." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo automatically blocks hidden trackers as you browse the web." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo bloquea automáticamente los rastreadores ocultos mientras navegas por la web." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo bloque automatiquement les traqueurs cachés lorsque vous naviguez sur le Web." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo blocca automaticamente i sistemi di tracciamento nascosti mentre navighi sul web." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo blokkeert automatisch verborgen trackers terwijl je op het web surft." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podczas przeglądania stron internetowych DuckDuckGo automatycznie blokuje ukryte mechanizmy śledzące." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo bloqueia automaticamente os rastreadores ocultos enquanto navegas na Internet." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo автоматически блокирует скрытые трекеры, пока вы посещаете сайты." + } + } + } + }, "web.tracking.protection.title" : { "comment" : "Web tracking protection settings section title", "extractionState" : "extracted_with_value", @@ -50162,9 +51198,6 @@ } } } - }, - "Zero" : { - }, "zoom" : { "comment" : "Menu with Zooming commands", diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index f75a983cca..75bcc66066 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -57,6 +57,8 @@ final class AddressBarTextField: NSTextField { private var addressBarStringCancellable: AnyCancellable? private var contentTypeCancellable: AnyCancellable? + private let searchPreferences: SearchPreferences = SearchPreferences.shared + private enum TextDidChangeEventType { case none case userAppendingTextToTheEnd @@ -624,9 +626,9 @@ final class AddressBarTextField: NSTextField { } @objc func toggleAutocomplete(_ menuItem: NSMenuItem) { - AppearancePreferences.shared.showAutocompleteSuggestions.toggle() + searchPreferences.showAutocompleteSuggestions.toggle() - let shouldShowAutocomplete = AppearancePreferences.shared.showAutocompleteSuggestions + let shouldShowAutocomplete = searchPreferences.showAutocompleteSuggestions menuItem.state = shouldShowAutocomplete ? .on : .off @@ -1084,7 +1086,7 @@ private extension NSMenuItem { action: #selector(AddressBarTextField.toggleAutocomplete(_:)), keyEquivalent: "" ) - menuItem.state = AppearancePreferences.shared.showAutocompleteSuggestions ? .on : .off + menuItem.state = SearchPreferences.shared.showAutocompleteSuggestions ? .on : .off return menuItem } diff --git a/DuckDuckGo/Preferences/Model/AboutModel.swift b/DuckDuckGo/Preferences/Model/AboutModel.swift index 7c44d07efd..52508bd9cb 100644 --- a/DuckDuckGo/Preferences/Model/AboutModel.swift +++ b/DuckDuckGo/Preferences/Model/AboutModel.swift @@ -19,7 +19,7 @@ import SwiftUI import Common -final class AboutModel: ObservableObject { +final class AboutModel: ObservableObject, PreferencesTabOpening { let appVersion = AppVersion() #if NETWORK_PROTECTION @@ -37,11 +37,6 @@ final class AboutModel: ObservableObject { let displayableAboutURL: String = URL.aboutDuckDuckGo .toString(decodePunycode: false, dropScheme: true, dropTrailingSlash: false) - @MainActor - func openURL(_ url: URL) { - WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) - } - @MainActor func openFeedbackForm() { FeedbackPresenter.presentFeedbackForm() diff --git a/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift b/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift new file mode 100644 index 0000000000..c096191757 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift @@ -0,0 +1,69 @@ +// +// AccessibilityPreferences.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AppKit +import Bookmarks +import Common + +protocol AccessibilityPreferencesPersistor { + var defaultPageZoom: CGFloat { get set } +} + +struct AccessibilityPreferencesUserDefaultsPersistor: AccessibilityPreferencesPersistor { + @UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue) + var defaultPageZoom: CGFloat +} + +enum DefaultZoomValue: CGFloat, CaseIterable { + case percent50 = 0.5 + case percent75 = 0.75 + case percent85 = 0.85 + case percent100 = 1.0 + case percent115 = 1.15 + case percent125 = 1.25 + case percent150 = 1.50 + case percent175 = 1.75 + case percent200 = 2.0 + case percent250 = 2.5 + case percent300 = 3.0 + + var displayString: String { + let percentage = (self.rawValue * 100).rounded() + return String(format: "%.0f%%", percentage) + } + + var index: Int {DefaultZoomValue.allCases.firstIndex(of: self) ?? 3} +} + +final class AccessibilityPreferences: ObservableObject { + static let shared = AccessibilityPreferences() + + @Published var defaultPageZoom: DefaultZoomValue { + didSet { + persistor.defaultPageZoom = defaultPageZoom.rawValue + } + } + + init(persistor: AccessibilityPreferencesPersistor = AccessibilityPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + defaultPageZoom = .init(rawValue: persistor.defaultPageZoom) ?? .percent100 + } + + private var persistor: AccessibilityPreferencesPersistor +} diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index ab73548d08..833bab73f0 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -23,9 +23,7 @@ import Common protocol AppearancePreferencesPersistor { var showFullURL: Bool { get set } - var showAutocompleteSuggestions: Bool { get set } var currentThemeName: String { get set } - var defaultPageZoom: CGFloat { get set } var favoritesDisplayMode: String? { get set } var isFavoriteVisible: Bool { get set } var isContinueSetUpVisible: Bool { get set } @@ -39,15 +37,9 @@ struct AppearancePreferencesUserDefaultsPersistor: AppearancePreferencesPersisto @UserDefaultsWrapper(key: .showFullURL, defaultValue: false) var showFullURL: Bool - @UserDefaultsWrapper(key: .showAutocompleteSuggestions, defaultValue: true) - var showAutocompleteSuggestions: Bool - @UserDefaultsWrapper(key: .currentThemeName, defaultValue: ThemeName.systemDefault.rawValue) var currentThemeName: String - @UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue) - var defaultPageZoom: CGFloat - @UserDefaultsWrapper(key: .favoritesDisplayMode, defaultValue: FavoritesDisplayMode.displayNative(.desktop).description) var favoritesDisplayMode: String? @@ -85,27 +77,6 @@ enum HomeButtonPosition: String, CaseIterable { case right } -enum DefaultZoomValue: CGFloat, CaseIterable { - case percent50 = 0.5 - case percent75 = 0.75 - case percent85 = 0.85 - case percent100 = 1.0 - case percent115 = 1.15 - case percent125 = 1.25 - case percent150 = 1.50 - case percent175 = 1.75 - case percent200 = 2.0 - case percent250 = 2.5 - case percent300 = 3.0 - - var displayString: String { - let percentage = (self.rawValue * 100).rounded() - return String(format: "%.0f%%", percentage) - } - - var index: Int {DefaultZoomValue.allCases.firstIndex(of: self) ?? 3} -} - enum ThemeName: String, Equatable, CaseIterable { case light case dark @@ -181,24 +152,12 @@ final class AppearancePreferences: ObservableObject { } } - @Published var showAutocompleteSuggestions: Bool { - didSet { - persistor.showAutocompleteSuggestions = showAutocompleteSuggestions - } - } - @Published var favoritesDisplayMode: FavoritesDisplayMode { didSet { persistor.favoritesDisplayMode = favoritesDisplayMode.description } } - @Published var defaultPageZoom: DefaultZoomValue { - didSet { - persistor.defaultPageZoom = defaultPageZoom.rawValue - } - } - @Published var isFavoriteVisible: Bool { didSet { persistor.isFavoriteVisible = isFavoriteVisible @@ -262,12 +221,10 @@ final class AppearancePreferences: ObservableObject { self.persistor = persistor currentThemeName = .init(rawValue: persistor.currentThemeName) ?? .systemDefault showFullURL = persistor.showFullURL - showAutocompleteSuggestions = persistor.showAutocompleteSuggestions favoritesDisplayMode = persistor.favoritesDisplayMode.flatMap(FavoritesDisplayMode.init) ?? .default isFavoriteVisible = persistor.isFavoriteVisible isRecentActivityVisible = persistor.isRecentActivityVisible isContinueSetUpVisible = persistor.isContinueSetUpVisible - defaultPageZoom = .init(rawValue: persistor.defaultPageZoom) ?? .percent100 showBookmarksBar = persistor.showBookmarksBar bookmarksBarAppearance = persistor.bookmarksBarAppearance homeButtonPosition = persistor.homeButtonPosition diff --git a/DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift b/DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift new file mode 100644 index 0000000000..3082f23a0a --- /dev/null +++ b/DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift @@ -0,0 +1,52 @@ +// +// CookiePopupProtectionPreferences.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AppKit +import Bookmarks +import Common + +protocol CookiePopupProtectionPreferencesPersistor { + var autoconsentEnabled: Bool { get set } +} + +struct CookiePopupProtectionPreferencesUserDefaultsPersistor: CookiePopupProtectionPreferencesPersistor { + + @UserDefaultsWrapper(key: .autoconsentEnabled, defaultValue: true) + var autoconsentEnabled: Bool + +} + +final class CookiePopupProtectionPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = CookiePopupProtectionPreferences() + + @Published + var isAutoconsentEnabled: Bool { + didSet { + persistor.autoconsentEnabled = isAutoconsentEnabled + } + } + + init(persistor: CookiePopupProtectionPreferencesPersistor = CookiePopupProtectionPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + isAutoconsentEnabled = persistor.autoconsentEnabled + } + + private var persistor: CookiePopupProtectionPreferencesPersistor +} diff --git a/DuckDuckGo/Preferences/Model/PrivacyPreferencesModel.swift b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift similarity index 51% rename from DuckDuckGo/Preferences/Model/PrivacyPreferencesModel.swift rename to DuckDuckGo/Preferences/Model/DataClearingPreferences.swift index 212336b3a8..e336ab7cbd 100644 --- a/DuckDuckGo/Preferences/Model/PrivacyPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift @@ -1,5 +1,5 @@ // -// PrivacyPreferencesModel.swift +// DataClearingPreferences.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -18,25 +18,14 @@ import Foundation -final class PrivacyPreferencesModel: ObservableObject { +final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { - @Published - var isLoginDetectionEnabled: Bool { - didSet { - privacySecurityPreferences.loginDetectionEnabled = isLoginDetectionEnabled - } - } + static let shared = DataClearingPreferences() @Published - var isGPCEnabled: Bool { - didSet { - privacySecurityPreferences.gpcEnabled = isGPCEnabled - } - } - - @Published var isAutoconsentEnabled: Bool { + var isLoginDetectionEnabled: Bool { didSet { - privacySecurityPreferences.autoconsentEnabled = isAutoconsentEnabled + persistor.loginDetectionEnabled = isLoginDetectionEnabled } } @@ -47,24 +36,28 @@ final class PrivacyPreferencesModel: ObservableObject { guard let fireproofDomainsWindow = fireproofDomainsWindowController.window, let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController else { - assertionFailure("Privacy Preferences: Failed to present FireproofDomainsViewController") + assertionFailure("DataClearingPreferences: Failed to present FireproofDomainsViewController") return } parentWindowController.window?.beginSheet(fireproofDomainsWindow) } - @MainActor - func openURL(_ url: URL) { - WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) + init(persistor: FireButtonPreferencesPersistor = FireButtonPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + isLoginDetectionEnabled = persistor.loginDetectionEnabled } - init(privacySecurityPreferences: PrivacySecurityPreferences = .shared) { - self.privacySecurityPreferences = privacySecurityPreferences - isLoginDetectionEnabled = privacySecurityPreferences.loginDetectionEnabled - isGPCEnabled = privacySecurityPreferences.gpcEnabled - isAutoconsentEnabled = privacySecurityPreferences.autoconsentEnabled - } + private var persistor: FireButtonPreferencesPersistor +} + +protocol FireButtonPreferencesPersistor { + var loginDetectionEnabled: Bool { get set } +} + +struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersistor { + + @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) + var loginDetectionEnabled: Bool - private let privacySecurityPreferences: PrivacySecurityPreferences } diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index ecaf76e24e..a40e390510 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -71,6 +71,8 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { final class DefaultBrowserPreferences: ObservableObject { + static let shared = DefaultBrowserPreferences() + @Published private(set) var isDefault: Bool = false { didSet { // Temporary pixel for first time user import data @@ -113,11 +115,37 @@ final class DefaultBrowserPreferences: ObservableObject { do { try defaultBrowserProvider.presentDefaultBrowserPrompt() + repeatCheckIfDefault() } catch { defaultBrowserProvider.openSystemPreferences() } } + var executionCount = 0 + let maxNumberOfExecutions = 60 + var timer: Timer? + + // Monitors for changes in default browser setting over the next minute. + // The reason is there is no API to get a notification for this change. + private func repeatCheckIfDefault() { + timer?.invalidate() + executionCount = 0 + timer = Timer.scheduledTimer(timeInterval: 1.0, + target: self, + selector: #selector(timerFired), + userInfo: nil, + repeats: true) + } + + @objc private func timerFired() { + checkIfDefault() + + executionCount += 1 + if executionCount >= maxNumberOfExecutions { + timer?.invalidate() + } + } + private var appDidBecomeActiveCancellable: AnyCancellable? private let defaultBrowserProvider: DefaultBrowserProvider } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index a928eb6202..785350403e 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -29,52 +29,79 @@ struct PreferencesSection: Hashable, Identifiable { @MainActor static func defaultSections(includingDuckPlayer: Bool, includingSync: Bool, includingVPN: Bool) -> [PreferencesSection] { - let regularPanes: [PreferencePaneIdentifier] = { - - var panes: [PreferencePaneIdentifier] = [.general, .appearance, .privacy, .autofill, .downloads] + var privacyPanes: [PreferencePaneIdentifier] = [.defaultBrowser, .privateSearch, .webTrackingProtection, .cookiePopupProtection, .emailProtection] - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { -#if SUBSCRIPTION - panes = [.privacy, .subscription, .general, .appearance, .autofill, .downloads] +#if NETWORK_PROTECTION + if includingVPN { + privacyPanes.append(.vpn) + } #endif - } + + let regularPanes: [PreferencePaneIdentifier] = { + var panes: [PreferencePaneIdentifier] = [.general, .appearance, .autofill, .accessibility, .dataClearing] if includingSync { - if let generalIndex = panes.firstIndex(of: .general) { - panes.insert(.sync, at: generalIndex + 1) - } + panes.insert(.sync, at: 1) } if includingDuckPlayer { panes.append(.duckPlayer) } -#if NETWORK_PROTECTION - if includingVPN { - panes.append(.vpn) - } -#endif - return panes }() - return [ + let otherPanes: [PreferencePaneIdentifier] = [.about, .otherPlatforms] + + var sections: [PreferencesSection] = [ + .init(id: .privacyProtections, panes: privacyPanes), .init(id: .regularPreferencePanes, panes: regularPanes), - .init(id: .about, panes: [.about]) + .init(id: .about, panes: otherPanes) ] + +#if SUBSCRIPTION + if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + let subscriptionPanes: [PreferencePaneIdentifier] = [.subscription] + sections.insert(.init(id: .privacyPro, panes: subscriptionPanes), at: 1) + } +#endif + + return sections } } enum PreferencesSectionIdentifier: Hashable, CaseIterable { + case privacyProtections + case privacyPro case regularPreferencePanes case about + + var displayName: String? { + switch self { + case .privacyProtections: + return UserText.privacyProtections + case .privacyPro: + return nil + case .regularPreferencePanes: + return UserText.mainSettings + case .about: + return nil + } + } + } enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { + case defaultBrowser + case privateSearch + case webTrackingProtection + case cookiePopupProtection + case emailProtection + case general case sync case appearance - case privacy + case dataClearing #if NETWORK_PROTECTION case vpn #endif @@ -82,8 +109,9 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { case subscription #endif case autofill - case downloads + case accessibility case duckPlayer = "duckplayer" + case otherPlatforms = "https://duckduckgo.com/app" case about var id: Self { @@ -106,6 +134,16 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { @MainActor var displayName: String { switch self { + case .defaultBrowser: + return UserText.defaultBrowser + case .privateSearch: + return UserText.privateSearch + case .webTrackingProtection: + return UserText.webTrackingProtection + case .cookiePopupProtection: + return UserText.cookiePopUpProtection + case .emailProtection: + return UserText.emailProtectionPreferences case .general: return UserText.general case .sync: @@ -119,8 +157,8 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { return UserText.sync case .appearance: return UserText.appearance - case .privacy: - return UserText.privacy + case .dataClearing: + return UserText.dataClearing #if NETWORK_PROTECTION case .vpn: return UserText.vpn @@ -131,25 +169,37 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { #endif case .autofill: return UserText.autofill - case .downloads: - return UserText.downloads + case .accessibility: + return UserText.accessibility case .duckPlayer: return UserText.duckPlayer case .about: return UserText.about + case .otherPlatforms: + return UserText.duckduckgoOnOtherPlatforms } } var preferenceIconName: String { switch self { + case .defaultBrowser: + return "DefaultBrowser" + case .privateSearch: + return "PrivateSearchIcon" + case .webTrackingProtection: + return "WebTrackingProtectionIcon" + case .cookiePopupProtection: + return "CookieProtectionIcon" + case .emailProtection: + return "EmailProtectionIcon" case .general: - return "Rocket" + return "GeneralIcon" case .sync: return "Sync" case .appearance: return "Appearance" - case .privacy: - return "Privacy" + case .dataClearing: + return "FireSettings" #if NETWORK_PROTECTION case .vpn: return "VPN" @@ -160,12 +210,14 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { #endif case .autofill: return "Autofill" - case .downloads: - return "DownloadsPreferences" + case .accessibility: + return "Accessibility" case .duckPlayer: return "DuckPlayerSettings" case .about: return "About" + case .otherPlatforms: + return "OtherPlatformsPreferences" } } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index de53764fec..97c5a06a42 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -27,7 +27,7 @@ final class PreferencesSidebarModel: ObservableObject { @Published private(set) var sections: [PreferencesSection] = [] @Published var selectedTabIndex: Int = 0 - @Published private(set) var selectedPane: PreferencePaneIdentifier = .general + @Published private(set) var selectedPane: PreferencePaneIdentifier = .defaultBrowser var selectedTabContent: AnyPublisher { $selectedTabIndex.map { [tabSwitcherTabs] in tabSwitcherTabs[$0] }.eraseToAnyPublisher() @@ -125,7 +125,16 @@ final class PreferencesSidebarModel: ObservableObject { } } + @MainActor func selectPane(_ identifier: PreferencePaneIdentifier) { + // Open a new tab in case of special panes + if identifier.rawValue.hasPrefix(URL.NavigationalScheme.https.rawValue), + let url = URL(string: identifier.rawValue) { + WindowControllersManager.shared.show(url: url, + source: .ui, + newTab: true) + } + if sections.flatMap(\.panes).contains(identifier), identifier != selectedPane { selectedPane = identifier } diff --git a/DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift b/DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift new file mode 100644 index 0000000000..67474fabe6 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift @@ -0,0 +1,77 @@ +// +// PrivacyProtectionStatus.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine +import BrowserServicesKit + +final class PrivacyProtectionStatus: ObservableObject { + + static func status(for preferencePane: PreferencePaneIdentifier) -> PrivacyProtectionStatus { + switch preferencePane { + case .defaultBrowser: + return PrivacyProtectionStatus(statusPublisher: DefaultBrowserPreferences.shared.$isDefault) { isDefault in + isDefault ? .on : .off + } + case .privateSearch: + return PrivacyProtectionStatus(statusIndicator: .on) + case .webTrackingProtection: + return PrivacyProtectionStatus(statusIndicator: .on) + case .cookiePopupProtection: + return PrivacyProtectionStatus(statusPublisher: CookiePopupProtectionPreferences.shared.$isAutoconsentEnabled) { isAutoconsentEnabled in + isAutoconsentEnabled ? .on : .off + } + case .emailProtection: + let publisher = Publishers.Merge( + NotificationCenter.default.publisher(for: .emailDidSignIn), + NotificationCenter.default.publisher(for: .emailDidSignOut) + ) + return PrivacyProtectionStatus(statusPublisher: publisher, initialValue: EmailManager().isSignedIn ? .on : .off) { _ in + EmailManager().isSignedIn ? .on : .off + } + default: + return PrivacyProtectionStatus() + } + } + + var statusSubscription: AnyCancellable? + @Published var status: Preferences.StatusIndicator? + + // Initializer for observable properties + init(statusPublisher: T, + initialValue: Preferences.StatusIndicator? = nil, + transform: @escaping (T.Output) -> Preferences.StatusIndicator?) where T.Failure == Never { + self.status = initialValue + + statusSubscription = statusPublisher + .map(transform) + .sink { [weak self] newStatus in + self?.status = newStatus + } + } + + // Initializer for items without a status + init() { + self.status = nil + } + + // Initializer for items with static status + init(statusIndicator: Preferences.StatusIndicator) { + self.status = statusIndicator + } +} diff --git a/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift b/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift deleted file mode 100644 index 96b8b2b4a0..0000000000 --- a/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PrivacySecurityPreferences.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Combine - -final class PrivacySecurityPreferences { - static let shared = PrivacySecurityPreferences() - - private init() {} - - @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) - var loginDetectionEnabled: Bool - - @Published - var gpcEnabled: Bool = UserDefaultsWrapper(key: .gpcEnabled, defaultValue: true).wrappedValue { - didSet { - let udWrapper = UserDefaultsWrapper(key: .gpcEnabled, defaultValue: true) - udWrapper.wrappedValue = gpcEnabled - } - } - - @UserDefaultsWrapper(key: .autoconsentEnabled, defaultValue: true) - var autoconsentEnabled: Bool - -} diff --git a/DuckDuckGo/Preferences/Model/SearchPreferences.swift b/DuckDuckGo/Preferences/Model/SearchPreferences.swift new file mode 100644 index 0000000000..bbe5cfb70c --- /dev/null +++ b/DuckDuckGo/Preferences/Model/SearchPreferences.swift @@ -0,0 +1,64 @@ +// +// SearchPreferences.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AppKit +import Bookmarks +import Common + +protocol SearchPreferencesPersistor { + var showAutocompleteSuggestions: Bool { get set } +} + +struct SearchPreferencesUserDefaultsPersistor: SearchPreferencesPersistor { + @UserDefaultsWrapper(key: .showAutocompleteSuggestions, defaultValue: true) + var showAutocompleteSuggestions: Bool +} + +final class SearchPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = SearchPreferences() + + @Published var showAutocompleteSuggestions: Bool { + didSet { + persistor.showAutocompleteSuggestions = showAutocompleteSuggestions + } + } + + init(persistor: SearchPreferencesPersistor = SearchPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + showAutocompleteSuggestions = persistor.showAutocompleteSuggestions + } + + private var persistor: SearchPreferencesPersistor +} + +protocol PreferencesTabOpening { + + func openNewTab(with url: URL) + +} + +extension PreferencesTabOpening { + + @MainActor + func openNewTab(with url: URL) { + WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) + } + +} diff --git a/DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift b/DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift new file mode 100644 index 0000000000..5ef1539af1 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift @@ -0,0 +1,52 @@ +// +// WebTrackingProtectionPreferences.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AppKit +import Bookmarks +import Common + +protocol WebTrackingProtectionPreferencesPersistor { + var gpcEnabled: Bool { get set } +} + +struct WebTrackingProtectionPreferencesUserDefaultsPersistor: WebTrackingProtectionPreferencesPersistor { + + @UserDefaultsWrapper(key: .gpcEnabled, defaultValue: true) + var gpcEnabled: Bool + +} + +final class WebTrackingProtectionPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = WebTrackingProtectionPreferences() + + @Published + var isGPCEnabled: Bool { + didSet { + persistor.gpcEnabled = isGPCEnabled + } + } + + init(persistor: WebTrackingProtectionPreferencesPersistor = WebTrackingProtectionPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + isGPCEnabled = persistor.gpcEnabled + } + + private var persistor: WebTrackingProtectionPreferencesPersistor +} diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index d039da2091..d298ef89fd 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -70,11 +70,11 @@ extension Preferences { .padding(.bottom, 8) TextButton(UserText.moreAt(url: model.displayableAboutURL)) { - model.openURL(.aboutDuckDuckGo) + model.openNewTab(with: .aboutDuckDuckGo) } TextButton(UserText.privacyPolicy) { - model.openURL(.privacyPolicy) + model.openNewTab(with: .privacyPolicy) } #if FEEDBACK diff --git a/DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift b/DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift new file mode 100644 index 0000000000..fa38e9e2dd --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift @@ -0,0 +1,54 @@ +// +// PreferencesAccessibilityView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct AccessibilityView: View { + @ObservedObject var model: AccessibilityPreferences + + var body: some View { + PreferencePane(UserText.accessibility) { + + // SECTION 1: Zoom Setting + PreferencePaneSection { + + HStack { + Text(UserText.zoomPickerTitle) + NSPopUpButtonView(selection: $model.defaultPageZoom) { + let button = NSPopUpButton() + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + for value in DefaultZoomValue.allCases { + let item = button.menu?.addItem(withTitle: value.displayString, action: nil, keyEquivalent: "") + item?.representedObject = value + } + return button + } + } + } + + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift index 8e9e46fdfd..be30d68da2 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift @@ -98,7 +98,6 @@ extension Preferences { // SECTION 2: Address Bar PreferencePaneSection(UserText.addressBar) { ToggleMenuItem(UserText.showFullWebsiteAddress, isOn: $model.showFullURL) - ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $model.showAutocompleteSuggestions) } // SECTION 3: New Tab Page @@ -131,24 +130,6 @@ extension Preferences { .disabled(!model.showBookmarksBar) } } - - // SECTION 5: Zoom Setting - PreferencePaneSection(UserText.zoomSettingTitle) { - - HStack { - Text(UserText.zoomPickerTitle) - NSPopUpButtonView(selection: $model.defaultPageZoom) { - let button = NSPopUpButton() - button.setContentHuggingPriority(.defaultHigh, for: .horizontal) - - for value in DefaultZoomValue.allCases { - let item = button.menu?.addItem(withTitle: value.displayString, action: nil, keyEquivalent: "") - item?.representedObject = value - } - return button - } - } - } } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift b/DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift new file mode 100644 index 0000000000..b1b22cd69c --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift @@ -0,0 +1,55 @@ +// +// PreferencesCookiePopupProtectionView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct CookiePopupProtectionView: View { + @ObservedObject var model: CookiePopupProtectionPreferences + + var body: some View { + PreferencePane(UserText.cookiePopUpProtection, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: model.isAutoconsentEnabled ? .on : .off, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.autoconsentExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .cookieConsentPopUpManagement) + } + } + } + + // SECTION 3: Search Settings + PreferencePaneSection { + ToggleMenuItem(UserText.autoconsentCheckboxTitle, isOn: $model.isAutoconsentEnabled) + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift new file mode 100644 index 0000000000..dc3f28607c --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift @@ -0,0 +1,54 @@ +// +// PreferencesDataClearingView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct DataClearingView: View { + @ObservedObject var model: DataClearingPreferences + + var body: some View { + PreferencePane(UserText.dataClearing) { + + // SECTION 1: Fireproof Site + PreferencePaneSection(UserText.fireproofSites) { + + PreferencePaneSubSection { + ToggleMenuItem(UserText.fireproofCheckboxTitle, isOn: $model.isLoginDetectionEnabled) + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.fireproofExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .theFireButton) + } + } + } + + PreferencePaneSubSection { + Button(UserText.manageFireproofSites) { + model.presentManageFireproofSitesDialog() + } + } + } + + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift b/DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift new file mode 100644 index 0000000000..cc7692b366 --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift @@ -0,0 +1,69 @@ +// +// PreferencesDefaultBrowserView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct DefaultBrowserView: View { + @ObservedObject var defaultBrowserModel: DefaultBrowserPreferences + let status: PrivacyProtectionStatus + + var body: some View { + PreferencePane(UserText.defaultBrowser, spacing: 4) { + + // SECTION 1: Status Indicator + if let status = status.status { + PreferencePaneSection { + StatusIndicatorView(status: status, isLarge: true) + } + } + + // SECTION 2: Default Browser + PreferencePaneSection { + + PreferencePaneSubSection { + HStack { + if defaultBrowserModel.isDefault { + Text(UserText.isDefaultBrowser) + } else { + HStack { + Image(.warning).foregroundColor(Color(.linkBlue)) + Text(UserText.isNotDefaultBrowser) + } + .padding(.trailing, 8) + Button(action: { + Pixel.fire(.defaultRequestedFromSettings) + defaultBrowserModel.becomeDefault() + }) { + Text(UserText.makeDefaultBrowser) + .fixedSize(horizontal: true, vertical: false) + .multilineTextAlignment(.center) + } + } + } + } + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift b/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift deleted file mode 100644 index 9ffd8ee3a4..0000000000 --- a/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// PreferencesDownloadsView.swift -// -// Copyright © 2022 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import PreferencesViews -import SwiftUI -import SwiftUIExtensions - -extension Preferences { - - struct DownloadsView: View { - @ObservedObject var model: DownloadsPreferences - - var body: some View { - PreferencePane(UserText.downloads) { - - PreferencePaneSubSection { - ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, - isOn: $model.shouldOpenPopupOnCompletion) - } - - // MARK: Location - PreferencePaneSection(UserText.downloadsLocation) { - - HStack { - NSPathControlView(url: model.selectedDownloadLocation) -#if !APPSTORE - Button(UserText.downloadsChangeDirectory) { - model.presentDownloadDirectoryPanel() - } -#endif - } - .disabled(model.alwaysRequestDownloadLocation) - ToggleMenuItem(UserText.downloadsAlwaysAsk, - isOn: $model.alwaysRequestDownloadLocation) - } - } - } - } -} - -#Preview { - VStack { - HStack { - Preferences.DownloadsView(model: DownloadsPreferences(persistor: DownloadsPreferencesUserDefaultsPersistor())) - .padding() - Spacer() - }.frame(width: 500) - - }.background(Color.preferencesBackground) -} diff --git a/DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift b/DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift new file mode 100644 index 0000000000..cf864f651e --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift @@ -0,0 +1,81 @@ +// +// PreferencesEmailProtectionView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions +import BrowserServicesKit + +extension Preferences { + struct EmailProtectionView: View, PreferencesTabOpening { + var emailManager: EmailManager + @ObservedObject var protectionStatus: PrivacyProtectionStatus = PrivacyProtectionStatus.status(for: .emailProtection) + + var body: some View { + PreferencePane(UserText.emailProtectionPreferences, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: protectionStatus.status ?? .off, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.emailProtectionExplanation) + TextButton(UserText.learnMore) { + openNewTab(with: .duckDuckGoEmailInfo) + } + } + } + + // SECTION 3: Current Account Info + PreferencePaneSection { + if emailManager.isSignedIn { + if let userEmail = emailManager.userEmail { + Text(UserText.autofillEnabledFor) + Text(" \(userEmail)").bold() + } + Button(UserText.emailOptionsMenuManageAccountSubItem + "…") { + openNewTab(with: EmailUrls().emailProtectionAccountLink) + } + Button(UserText.emailOptionsMenuTurnOffSubItem) { + let alert = NSAlert.disableEmailProtection() + let response = alert.runModal() + if response == .alertFirstButtonReturn { + try? emailManager.signOut() + } + } + + // Support + PreferencePaneSubSection { + TextButton(UserText.support) { + openNewTab(with: EmailUrls().emailProtectionSupportLink) + } + } + } else { + Button(UserText.emailOptionsMenuTurnOnSubItem + "…") { + openNewTab(with: EmailUrls().emailProtectionLink) + } + } + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 6ad97d6463..591961ee75 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -25,38 +25,15 @@ import SwiftUIExtensions extension Preferences { struct GeneralView: View { - @ObservedObject var defaultBrowserModel: DefaultBrowserPreferences @ObservedObject var startupModel: StartupPreferences + @ObservedObject var downloadsModel: DownloadsPreferences + @ObservedObject var searchModel: SearchPreferences @State private var showingCustomHomePageSheet = false var body: some View { PreferencePane(UserText.general) { - // SECTION 1: Default Browser - PreferencePaneSection(UserText.defaultBrowser) { - - PreferencePaneSubSection { - HStack { - if defaultBrowserModel.isDefault { - Image(.solidCheckmark) - Text(UserText.isDefaultBrowser) - } else { - Image(.warning).foregroundColor(Color(.linkBlue)) - Text(UserText.isNotDefaultBrowser) - Button(action: { - defaultBrowserModel.becomeDefault() - }) { - Text(UserText.makeDefaultBrowser) - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.center) - .lineLimit(2) - } - } - } - } - } - - // SECTION 2: On Startup + // SECTION 1: On Startup PreferencePaneSection(UserText.onStartup) { PreferencePaneSubSection { @@ -70,7 +47,7 @@ extension Preferences { } } - // SECTION 3: Home Page + // SECTION 2: Home Page PreferencePaneSection(UserText.homePage) { PreferencePaneSubSection { @@ -103,7 +80,7 @@ extension Preferences { Text(UserText.homeButtonMode(for: position)).tag(position) } } - .scaledToFit() + .fixedSize() .onChange(of: startupModel.homeButtonPosition) { _ in startupModel.updateHomeButton() } @@ -114,6 +91,34 @@ extension Preferences { CustomHomePageSheet(startupModel: startupModel, isSheetPresented: $showingCustomHomePageSheet) } + // SECTION 3: Search Settings + PreferencePaneSection(UserText.privateSearch) { + ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions) + } + + // SECTION 4: Downloads + PreferencePaneSection(UserText.downloads) { + PreferencePaneSubSection { + ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, + isOn: $downloadsModel.shouldOpenPopupOnCompletion) + }.padding(.bottom, 5) + + // MARK: Location + PreferencePaneSubSection { + Text(UserText.downloadsLocation).bold() + HStack { + NSPathControlView(url: downloadsModel.selectedDownloadLocation) +#if !APPSTORE + Button(UserText.downloadsChangeDirectory) { + downloadsModel.presentDownloadDirectoryPanel() + } +#endif + } + .disabled(downloadsModel.alwaysRequestDownloadLocation) + ToggleMenuItem(UserText.downloadsAlwaysAsk, + isOn: $downloadsModel.alwaysRequestDownloadLocation) + } + } } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift b/DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift deleted file mode 100644 index 22755196e7..0000000000 --- a/DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// PreferencesPrivacyView.swift -// -// Copyright © 2022 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import PreferencesViews -import SwiftUI -import SwiftUIExtensions - -extension Preferences { - - struct PrivacyView: View { - @ObservedObject var model: PrivacyPreferencesModel - - var body: some View { - PreferencePane(UserText.privacy) { - - // SECTION 1: Web Tracking Protection Section - PreferencePaneSection(UserText.webTrackingProtectionSettingsTitle) { - - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.webTrackingProtectionExplenation) - TextButton(UserText.learnMore) { - model.openURL(.webTrackingProtection) - } - } - } - - // SECTION 2: Cookie Consent Pop-ups - PreferencePaneSection(UserText.autoconsentSettingsTitle) { - - ToggleMenuItem(UserText.autoconsentCheckboxTitle, isOn: $model.isAutoconsentEnabled) - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.autoconsentExplanation) - TextButton(UserText.learnMore) { - model.openURL(.cookieConsentPopUpManagement) - } - } - } - - // SECTION 3: Fireproof Site - PreferencePaneSection(UserText.fireproofSites) { - - PreferencePaneSubSection { - ToggleMenuItem(UserText.fireproofCheckboxTitle, isOn: $model.isLoginDetectionEnabled) - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.fireproofExplanation) - TextButton(UserText.learnMore) { - model.openURL(.theFireButton) - } - } - } - - PreferencePaneSubSection { - Button(UserText.manageFireproofSites) { - model.presentManageFireproofSitesDialog() - } - } - } - - // SECTION 4: Global privacy control - PreferencePaneSection(UserText.gpcSettingsTitle) { - - ToggleMenuItem(UserText.gpcCheckboxTitle, isOn: $model.isGPCEnabled) - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.gpcExplanation) - TextButton(UserText.learnMore) { - model.openURL(.gpcLearnMore) - } - } - } - } - } - } -} diff --git a/DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift b/DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift new file mode 100644 index 0000000000..b43f5db8fd --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift @@ -0,0 +1,55 @@ +// +// PreferencesPrivateSearchView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct PrivateSearchView: View { + @ObservedObject var model: SearchPreferences + + var body: some View { + PreferencePane(UserText.privateSearch, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: .alwaysOn, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.privateSearchExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .privateSearchLearnMore) + } + } + } + + // SECTION 3: Search Settings + PreferencePaneSection { + ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $model.showAutocompleteSuggestions) + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 76c6d00af1..5b01e77577 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -21,6 +21,7 @@ import PreferencesViews import SwiftUI import SwiftUIExtensions import SyncUI +import BrowserServicesKit #if SUBSCRIPTION import Subscription @@ -30,9 +31,16 @@ import SubscriptionUI enum Preferences { enum Const { - static let sidebarWidth: CGFloat = 256 + static var sidebarWidth: CGFloat { + switch Locale.current.languageCode { + case "en": + return 310 + default: + return 355 + } + } static let paneContentWidth: CGFloat = 524 - static let panePaddingHorizontal: CGFloat = 48 + static let panePaddingHorizontal: CGFloat = 40 static let panePaddingVertical: CGFloat = 40 } @@ -55,58 +63,75 @@ enum Preferences { var body: some View { HStack(spacing: 0) { Sidebar().environmentObject(model).frame(width: Const.sidebarWidth) - Color(NSColor.separatorColor).frame(width: 1) - ScrollView(.vertical) { HStack(spacing: 0) { - VStack(alignment: .leading) { - - switch model.selectedPane { - case .general: - GeneralView(defaultBrowserModel: DefaultBrowserPreferences(), startupModel: StartupPreferences.shared) - case .sync: - SyncView() - case .appearance: - AppearanceView(model: .shared) - case .privacy: - PrivacyView(model: PrivacyPreferencesModel()) + contentView + Spacer() + } + } + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.preferencesBackground) + } + + @ViewBuilder + var contentView: some View { + VStack(alignment: .leading) { + switch model.selectedPane { + case .defaultBrowser: + DefaultBrowserView(defaultBrowserModel: DefaultBrowserPreferences.shared, + status: PrivacyProtectionStatus.status(for: .defaultBrowser)) + case .privateSearch: + PrivateSearchView(model: SearchPreferences.shared) + case .webTrackingProtection: + WebTrackingProtectionView(model: WebTrackingProtectionPreferences.shared) + case .cookiePopupProtection: + CookiePopupProtectionView(model: CookiePopupProtectionPreferences.shared) + case .emailProtection: + EmailProtectionView(emailManager: EmailManager()) + case .general: + GeneralView(startupModel: StartupPreferences.shared, + downloadsModel: DownloadsPreferences.shared, + searchModel: SearchPreferences.shared) + case .sync: + SyncView() + case .appearance: + AppearanceView(model: .shared) + case .dataClearing: + DataClearingView(model: DataClearingPreferences.shared) #if NETWORK_PROTECTION - case .vpn: - VPNView(model: VPNPreferencesModel()) + case .vpn: + VPNView(model: VPNPreferencesModel()) #endif #if SUBSCRIPTION - case .subscription: - SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) + case .subscription: + SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) #endif - case .autofill: - AutofillView(model: AutofillPreferencesModel()) - case .downloads: - DownloadsView(model: .shared) - case .duckPlayer: - DuckPlayerView(model: .shared) - case .about: + case .autofill: + AutofillView(model: AutofillPreferencesModel()) + case .accessibility: + AccessibilityView(model: AccessibilityPreferences.shared) + case .duckPlayer: + DuckPlayerView(model: .shared) + case .otherPlatforms: + // Opens a new tab + Spacer() + case .about: #if NETWORK_PROTECTION - let netPInvitePresenter = NetworkProtectionInvitePresenter() - AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) + let netPInvitePresenter = NetworkProtectionInvitePresenter() + AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) #else - AboutView(model: AboutModel()) + AboutView(model: AboutModel()) #endif - } - } - .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) - .padding(.vertical, Const.panePaddingVertical) - .padding(.horizontal, Const.panePaddingHorizontal) - - Spacer() - } } - .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.preferencesBackground) + .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) + .padding(.vertical, Const.panePaddingVertical) + .padding(.horizontal, Const.panePaddingHorizontal) } #if SUBSCRIPTION @@ -168,27 +193,3 @@ enum Preferences { #endif } } - -struct SyncView: View { - - var body: some View { - if let syncService = NSApp.delegateTyped.syncService, let syncDataProviders = NSApp.delegateTyped.syncDataProviders { - SyncUI.ManagementView(model: SyncPreferences(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter)) - .onAppear { - requestSync() - } - } else { - FailedAssertionView("Failed to initialize Sync Management View") - } - } - - private func requestSync() { - Task { @MainActor in - guard let syncService = (NSApp.delegate as? AppDelegate)?.syncService else { - return - } - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") - syncService.scheduler.notifyDataChanged() - } - } -} diff --git a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift index 1a0c1babc6..b6f65c0a44 100644 --- a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift +++ b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift @@ -22,22 +22,109 @@ import SwiftUIExtensions extension Preferences { - struct SidebarItem: View { + struct SidebarSectionHeader: View { + let section: PreferencesSectionIdentifier + + var body: some View { + Group { + if let name = section.displayName { + Text(name) + .padding(.horizontal, 16) + .padding(.bottom, 3) + .font(PreferencesViews.Const.Fonts.sideBarHeader) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, minHeight: 31, alignment: .leading) + } + } + } + } + + struct PaneSidebarItem: View { let pane: PreferencePaneIdentifier let isSelected: Bool let action: () -> Void + @ObservedObject var protectionStatus: PrivacyProtectionStatus + + init(pane: PreferencePaneIdentifier, isSelected: Bool, action: @escaping () -> Void) { + self.pane = pane + self.isSelected = isSelected + self.action = action + self.protectionStatus = PrivacyProtectionStatus.status(for: pane) + } var body: some View { Button(action: action) { HStack(spacing: 6) { Image(pane.preferenceIconName).frame(width: 16, height: 16) Text(pane.displayName).font(PreferencesViews.Const.Fonts.sideBarItem) + + Spacer() + + if let status = protectionStatus.status { + StatusIndicatorView(status: status) + } } } .buttonStyle(SidebarItemButtonStyle(isSelected: isSelected)) } } + enum StatusIndicator: Equatable { + case alwaysOn + case on + case off + case custom(String) + + var text: String { + switch self { + case .alwaysOn: + return UserText.preferencesAlwaysOn + case .on: + return UserText.preferencesOn + case .off: + return UserText.preferencesOff + case .custom(let customText): + return customText + } + } + } + + struct StatusIndicatorView: View { + var status: StatusIndicator + var isLarge: Bool = false + + private var fontSize: CGFloat { + isLarge ? 13 : 10 + } + + private var circleSize: CGFloat { + isLarge ? 7 : 5 + } + + var body: some View { + HStack(spacing: isLarge ? 6 : 4) { + Circle() + .frame(width: circleSize, height: circleSize) + .foregroundColor(colorForStatus(status)) + + Text(status.text) + .font(.system(size: fontSize)) + .foregroundColor(.secondary) + } + } + + private func colorForStatus(_ status: StatusIndicator) -> Color { + switch status { + case .on, .alwaysOn: + return .alertGreen + case .off: + return Color.secondary.opacity(0.33) + case .custom: + return .orange + } + } + } + struct TabSwitcher: View { @EnvironmentObject var model: PreferencesSidebarModel @@ -61,7 +148,7 @@ extension Preferences { return button }) .padding(.horizontal, 3) - .frame(height: 60) + .frame(height: 51) .onAppear(perform: model.resetTabSelectionIfNeeded) } } @@ -77,20 +164,22 @@ extension Preferences { ScrollView { VStack(spacing: 0) { ForEach(model.sections) { section in + SidebarSectionHeader(section: section.id) sidebarSection(section) } - } + }.padding(.bottom, 16) } } .padding(.top, 18) - .padding(.horizontal, 20) + .padding(.horizontal, 10) } @ViewBuilder private func sidebarSection(_ section: PreferencesSection) -> some View { ForEach(section.panes) { pane in - SidebarItem(pane: pane, isSelected: model.selectedPane == pane) { + PaneSidebarItem(pane: pane, + isSelected: model.selectedPane == pane) { model.selectPane(pane) } } diff --git a/DuckDuckGo/Preferences/View/PreferencesSyncView.swift b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift new file mode 100644 index 0000000000..dab27f7101 --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift @@ -0,0 +1,47 @@ +// +// PreferencesSyncView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Common +import SyncUI +import SwiftUIExtensions +import BrowserServicesKit + +struct SyncView: View { + + var body: some View { + if let syncService = NSApp.delegateTyped.syncService, let syncDataProviders = NSApp.delegateTyped.syncDataProviders { + SyncUI.ManagementView(model: SyncPreferences(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter)) + .onAppear { + requestSync() + } + } else { + FailedAssertionView("Failed to initialize Sync Management View") + } + } + + private func requestSync() { + Task { @MainActor in + guard let syncService = (NSApp.delegate as? AppDelegate)?.syncService else { + return + } + os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + syncService.scheduler.notifyDataChanged() + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift b/DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift new file mode 100644 index 0000000000..90b5fa821a --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift @@ -0,0 +1,61 @@ +// +// PreferencesWebTrackingProtectionView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct WebTrackingProtectionView: View { + @ObservedObject var model: WebTrackingProtectionPreferences + + var body: some View { + PreferencePane(UserText.webTrackingProtection, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: .alwaysOn, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.webTrackingProtectionExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .webTrackingProtection) + } + } + } + + // SECTION 3: Global privacy control + PreferencePaneSection { + ToggleMenuItem(UserText.gpcCheckboxTitle, isOn: $model.isGPCEnabled) + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.gpcExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .gpcLearnMore) + } + }.padding(.leading, 19) + } + } + } + } +} diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index d30d1700d7..6074ac2343 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -221,7 +221,7 @@ extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { switch target { case .cookiePopupManagement: - tabCollection.appendNewTab(with: .settings(pane: .privacy), selected: true) + tabCollection.appendNewTab(with: .settings(pane: .dataClearing), selected: true) default: tabCollection.appendNewTab(with: .anySettingsPane, selected: true) } @@ -339,7 +339,7 @@ extension PrivacyDashboardViewController { tdsETag: ContentBlocking.shared.contentBlockingManager.currentRules.first?.etag, blockedTrackerDomains: blockedTrackerDomains, installedSurrogates: installedSurrogates, - isGPCEnabled: PrivacySecurityPreferences.shared.gpcEnabled, + isGPCEnabled: WebTrackingProtectionPreferences.shared.isGPCEnabled, ampURL: ampURL, urlParametersRemoved: urlParametersRemoved, protectionsState: protectionsState, diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index cc1ae4fcba..4647fd79a8 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -279,7 +279,7 @@ final class SaveCredentialsViewController: NSViewController { delegate?.shouldCloseSaveCredentialsViewController(self) } - guard PrivacySecurityPreferences.shared.loginDetectionEnabled else { + guard DataClearingPreferences.shared.isLoginDetectionEnabled else { notifyDelegate() return } diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 0af3664f30..bd576d9bf0 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -258,6 +258,12 @@ extension Pixel { case dailyPixel(Event, isFirst: Bool) + // Default Browser + case defaultRequestedFromHomepage + case defaultRequestedFromHomepageSetupView + case defaultRequestedFromSettings + case defaultRequestedFromOnboarding + case protectionToggledOffBreakageReport case toggleProtectionsDailyCount case toggleReportDoNotSend @@ -639,6 +645,11 @@ extension Pixel.Event { case .networkProtectionGeoswitchingNoLocations: return "m_mac_netp_ev_geoswitching_no_locations" + case .defaultRequestedFromHomepage: return "m_mac_default_requested_from_homepage" + case .defaultRequestedFromHomepageSetupView: return "m_mac_default_requested_from_homepage_setup_view" + case .defaultRequestedFromSettings: return "m_mac_default_requested_from_settings" + case .defaultRequestedFromOnboarding: return "m_mac_default_requested_from_onboarding" + // MARK: - Subscription case .privacyProSubscriptionActive: return "m_mac_\(appDistribution)_privacy-pro_app_subscription_active" case .privacyProOfferScreenImpression: return "m_mac_\(appDistribution)_privacy-pro_offer_screen_impression" @@ -682,6 +693,7 @@ extension Pixel.Event { case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" case .toggleReportDoNotSend: return "m_mac_toggle-report-do-not-send" case .toggleReportDismiss: return "m_mac_toggle-report-dismiss" + } } } diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index c8be054048..bca6af9060 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -166,6 +166,10 @@ extension Pixel.Event { .dataBrokerEnableLoginItemDaily, .dataBrokerDisableLoginItemDaily, .dataBrokerResetLoginItemDaily, + .defaultRequestedFromHomepage, + .defaultRequestedFromHomepageSetupView, + .defaultRequestedFromSettings, + .defaultRequestedFromOnboarding, .privacyProSubscriptionActive, .privacyProOfferScreenImpression, .privacyProPurchaseAttempt, diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift index 8d205d7fcc..8ad5149a9b 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift @@ -75,7 +75,7 @@ final class SuggestionContainerViewModel { } func setUserStringValue(_ userStringValue: String, userAppendedStringToTheEnd: Bool) { - guard AppearancePreferences.shared.showAutocompleteSuggestions else { + guard SearchPreferences.shared.showAutocompleteSuggestions else { return } diff --git a/DuckDuckGo/Tab/Model/UserContentUpdating.swift b/DuckDuckGo/Tab/Model/UserContentUpdating.swift index ba9f8ba7b6..721608ad24 100644 --- a/DuckDuckGo/Tab/Model/UserContentUpdating.swift +++ b/DuckDuckGo/Tab/Model/UserContentUpdating.swift @@ -50,13 +50,13 @@ final class UserContentUpdating { privacyConfigurationManager: PrivacyConfigurationManaging, trackerDataManager: TrackerDataManager, configStorage: ConfigurationStoring, - privacySecurityPreferences: PrivacySecurityPreferences, + webTrackingProtectionPreferences: WebTrackingProtectionPreferences, tld: TLD) { let makeValue: (Update) -> NewContent = { rulesUpdate in let sourceProvider = ScriptSourceProvider(configStorage: configStorage, privacyConfigurationManager: privacyConfigurationManager, - privacySettings: privacySecurityPreferences, + webTrackingProtectionPreferences: webTrackingProtectionPreferences, contentBlockingManager: contentBlockerRulesManager, trackerDataManager: trackerDataManager, tld: tld) @@ -78,7 +78,7 @@ final class UserContentUpdating { // 1. Collect updates from ContentBlockerRulesManager and generate UserScripts based on its output cancellable = contentBlockerRulesManager.updatesPublisher // regenerate UserScripts on gpcEnabled preference updated - .combineLatest(privacySecurityPreferences.$gpcEnabled) + .combineLatest(webTrackingProtectionPreferences.$isGPCEnabled) .map { $0.0 } // drop gpcEnabled value: $0.1 .combineLatest(onNotificationWithInitial(.autofillUserSettingsDidChange), combine) .combineLatest(onNotificationWithInitial(.autofillScriptDebugSettingsDidChange), combine) diff --git a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift index e4b1c54fa2..74974a747d 100644 --- a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift @@ -198,7 +198,7 @@ extension AutofillTabExtension: SecureVaultManagerDelegate { supportedFeatures.passwordGeneration = false } - return ContentScopeProperties(gpcEnabled: PrivacySecurityPreferences.shared.gpcEnabled, + return ContentScopeProperties(gpcEnabled: WebTrackingProtectionPreferences.shared.isGPCEnabled, sessionKey: autofillScript?.sessionKey ?? "", featureToggles: supportedFeatures) } diff --git a/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift index 539f7a29e3..aa8fa230fc 100644 --- a/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift @@ -101,9 +101,10 @@ extension NavigationProtectionTabExtension: NavigationResponder { request = newRequest } + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled if let newRequest = GPCRequestFactory().requestForGPC(basedOn: request, config: contentBlocking.privacyConfigurationManager.privacyConfig, - gpcEnabled: PrivacySecurityPreferences.shared.gpcEnabled) { + gpcEnabled: isGPCEnabled) { request = newRequest } diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 4cde694a07..4dfa1c5b50 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -50,10 +50,10 @@ final class UserScripts: UserScriptsProvider { clickToLoadScript = ClickToLoadUserScript(scriptSourceProvider: sourceProvider) contentBlockerRulesScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig!) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig!) - let privacySettings = PrivacySecurityPreferences.shared + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let privacyConfig = sourceProvider.privacyConfigurationManager.privacyConfig let sessionKey = sourceProvider.sessionKey ?? "" - let prefs = ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + let prefs = ContentScopeProperties(gpcEnabled: isGPCEnabled, sessionKey: sessionKey, featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig)) contentScopeUserScript = ContentScopeUserScript(sourceProvider.privacyConfigurationManager, properties: prefs) diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 37b06c15d5..eccbde153a 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -1092,12 +1092,13 @@ extension BrowserTabViewController: OnboardingDelegate { } func onboardingDidRequestSetDefault(completion: @escaping () -> Void) { - let defaultBrowserPreferences = DefaultBrowserPreferences() + let defaultBrowserPreferences = DefaultBrowserPreferences.shared if defaultBrowserPreferences.isDefault { completion() return } + Pixel.fire(.defaultRequestedFromOnboarding) defaultBrowserPreferences.becomeDefault { _ in _ = defaultBrowserPreferences withAnimation { diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 9d6fea5595..c6853be3fb 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -35,6 +35,7 @@ final class TabViewModel { private(set) var tab: Tab private let appearancePreferences: AppearancePreferences + private let accessibilityPreferences: AccessibilityPreferences private var cancellables = Set() @Published private(set) var canGoForward: Bool = false @@ -78,9 +79,12 @@ final class TabViewModel { !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } - init(tab: Tab, appearancePreferences: AppearancePreferences = .shared) { + init(tab: Tab, + appearancePreferences: AppearancePreferences = .shared, + accessibilityPreferences: AccessibilityPreferences = .shared) { self.tab = tab self.appearancePreferences = appearancePreferences + self.accessibilityPreferences = accessibilityPreferences subscribeToUrl() subscribeToCanGoBackForwardAndReload() @@ -88,7 +92,7 @@ final class TabViewModel { subscribeToFavicon() subscribeToTabError() subscribeToPermissions() - subscribeToAppearancePreferences() + subscribeToPreferences() subscribeToWebViewDidFinishNavigation() tab.$isLoading .assign(to: \.isLoading, onWeaklyHeld: self) @@ -209,12 +213,12 @@ final class TabViewModel { .store(in: &cancellables) } - private func subscribeToAppearancePreferences() { + private func subscribeToPreferences() { appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } self.updatePassiveAddressBarString(showURL: newValue, url: url, hostURL: host) }.store(in: &cancellables) - appearancePreferences.$defaultPageZoom.sink { [weak self] newValue in + accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } self.tab.webView.defaultZoomValue = newValue self.tab.webView.zoomLevel = newValue diff --git a/IntegrationTests/AutoconsentIntegrationTests.swift b/IntegrationTests/AutoconsentIntegrationTests.swift index b674b41635..63f54da582 100644 --- a/IntegrationTests/AutoconsentIntegrationTests.swift +++ b/IntegrationTests/AutoconsentIntegrationTests.swift @@ -39,7 +39,7 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor override func setUp() { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false window = WindowsManager.openNewWindow(with: Tab(content: .none)) } @@ -49,7 +49,7 @@ class AutoconsentIntegrationTests: XCTestCase { window.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests @@ -57,9 +57,8 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor func testWhenAutoconsentEnabled_cookieConsentManaged() async throws { // enable the feature - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true let url = URL(string: "http://privacy-test-pages.site/features/autoconsent/")! - let tab = self.tabViewModel.tab // expect cookieConsentManaged to be published @@ -84,9 +83,8 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor func testCosmeticRule_whenFakeCookieBannerIsDisplayed_bannerIsHidden() async throws { // enable the feature - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true let url = URL(string: "http://privacy-test-pages.site/features/autoconsent/banner.html")! - let tab = self.tabViewModel.tab // expect `cosmetic` to be published let cookieConsentManagedPromise = tab.privacyInfoPublisher @@ -135,9 +133,8 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor func testCosmeticRule_whenFakeCookieBannerIsDisplayedAndScriptsAreReloaded_bannerIsHidden() async throws { // enable the feature - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true let url = URL(string: "http://privacy-test-pages.site/features/autoconsent/banner.html")! - let tab = self.tabViewModel.tab // expect `cosmetic` to be published let cookieConsentManagedPromise = tab.privacyInfoPublisher @@ -160,8 +157,8 @@ class AutoconsentIntegrationTests: XCTestCase { os_log("navigationResponse: %s", "\(String(describing: response))") // cause UserScripts reload (ContentBlockingUpdating) - PrivacySecurityPreferences.shared.gpcEnabled = true - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = false return .allow }) diff --git a/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift b/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift index 14c8527e8a..9dbc287a0a 100644 --- a/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift +++ b/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift @@ -38,7 +38,7 @@ class HTTPSUpgradeIntegrationTests: XCTestCase { @MainActor override func setUp() async throws { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false window = WindowsManager.openNewWindow(with: .none)! @@ -51,7 +51,7 @@ class HTTPSUpgradeIntegrationTests: XCTestCase { window.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests diff --git a/IntegrationTests/History/HistoryIntegrationTests.swift b/IntegrationTests/History/HistoryIntegrationTests.swift index 33d06a3436..9a59e297f0 100644 --- a/IntegrationTests/History/HistoryIntegrationTests.swift +++ b/IntegrationTests/History/HistoryIntegrationTests.swift @@ -63,7 +63,7 @@ class HistoryIntegrationTests: XCTestCase { override func tearDown() async throws { window?.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests @@ -199,7 +199,7 @@ class HistoryIntegrationTests: XCTestCase { @MainActor func testWhenScriptTrackerLoaded_trackerAddedToHistory() async throws { - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false let tab = Tab(content: .newtab) window = WindowsManager.openNewWindow(with: tab)! @@ -227,7 +227,7 @@ class HistoryIntegrationTests: XCTestCase { @MainActor func testWhenSurrogateTrackerLoaded_trackerAddedToHistory() async throws { - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false let tab = Tab(content: .newtab) window = WindowsManager.openNewWindow(with: tab)! diff --git a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift index 13417aaecb..f36748c64b 100644 --- a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift +++ b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift @@ -47,7 +47,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { window?.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests @@ -55,7 +55,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { @MainActor func testAMPLinks() async throws { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false var onDidCancel: ((NavigationAction, [ExpectedNavigation]?) -> Void)? var onWillStart: ((Navigation) -> Void)? @@ -132,7 +132,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { @MainActor func testReferrerTrimming() async throws { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false var lastRedirectedNavigation: Navigation? var onDidFinish: ((Navigation) -> Void)? @@ -205,11 +205,11 @@ class NavigationProtectionIntegrationTests: XCTestCase { let url = URL(string: "https://privacy-test-pages.site/privacy-protections/gpc/")! // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false _=try await tab.setUrl(url, source: .link)?.result.get() // enable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true // expect popup to open and then close var oldValue: TabViewModel! = self.tabViewModel diff --git a/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift b/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift index 169b967391..6af50b4681 100644 --- a/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift +++ b/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift @@ -33,7 +33,7 @@ class PrivacyDashboardIntegrationTests: XCTestCase { @MainActor override func setUp() { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false window = WindowsManager.openNewWindow(with: .none)! } @@ -43,7 +43,7 @@ class PrivacyDashboardIntegrationTests: XCTestCase { window.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift index 7c15b69516..beb1f64ec6 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift @@ -34,6 +34,7 @@ public enum Const { public enum Fonts { public static let popUpButton: NSFont = .preferredFont(forTextStyle: .title1, options: [:]) + public static let sideBarHeader: Font = .system(size: 11) public static let sideBarItem: Font = .body public static let preferencePaneTitle: Font = .title2.weight(.semibold) public static let preferencePaneSectionHeader: Font = .title3.weight(.semibold) diff --git a/NetworkProtectionAppExtension/InfoPlist.xcstrings b/NetworkProtectionAppExtension/InfoPlist.xcstrings new file mode 100644 index 0000000000..cd83eb4310 --- /dev/null +++ b/NetworkProtectionAppExtension/InfoPlist.xcstrings @@ -0,0 +1,186 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "NetworkProtectionAppExtension" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "NetworkProtectionAppExtension" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + } + } + }, + "NSHumanReadableCopyright" : { + "comment" : "Copyright (human-readable)", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Alle Rechte vorbehalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copyright © 2023 DuckDuckGo. All rights reserved." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Todos los derechos reservados." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Tous droits réservés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Tutti i diritti riservati." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Alle rechten voorbehouden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Wszelkie prawa zastrzeżone." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Todos os direitos reservados." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "© 2023 DuckDuckGo. Все права защищены." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift b/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift index d8a1a6f356..a3cc50adfc 100644 --- a/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift +++ b/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift @@ -26,7 +26,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { let userScript = AutoconsentUserScript( scriptSource: ScriptSourceProvider(configStorage: MockConfigurationStore(), privacyConfigurationManager: MockPrivacyConfigurationManager(), - privacySettings: PrivacySecurityPreferences.shared, // todo: mock + webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, // mock contentBlockingManager: ContentBlockerRulesManagerMock(), trackerDataManager: TrackerDataManager(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), data: ConfigurationStore.shared.loadData(for: .trackerDataSet), @@ -38,7 +38,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { override func setUp() { super.setUp() - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true } func replyToJson(msg: Any) -> String { diff --git a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift index 79020eea36..08421e6bc4 100644 --- a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift +++ b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift @@ -25,7 +25,7 @@ import BrowserServicesKit final class ContentBlockingUpdatingTests: XCTestCase { - let preferences = PrivacySecurityPreferences.shared + let preferences = WebTrackingProtectionPreferences.shared let rulesManager = ContentBlockerRulesManagerMock() var updating: UserContentUpdating! @@ -37,7 +37,7 @@ final class ContentBlockingUpdatingTests: XCTestCase { embeddedDataProvider: AppTrackerDataSetProvider(), errorReporting: nil), configStorage: MockConfigurationStore(), - privacySecurityPreferences: preferences, + webTrackingProtectionPreferences: preferences, tld: TLD()) } @@ -107,7 +107,7 @@ final class ContentBlockingUpdatingTests: XCTestCase { } rulesManager.updatesSubject.send(Self.testUpdate()) - preferences.gpcEnabled = !preferences.gpcEnabled + preferences.isGPCEnabled = !preferences.isGPCEnabled withExtendedLifetime(c) { waitForExpectations(timeout: 0, handler: nil) diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 817fc5bfb0..c182554a38 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -68,8 +68,8 @@ final class ContinueSetUpModelTests: XCTestCase { var tabCollectionVM: TabCollectionViewModel! var emailManager: EmailManager! var emailStorage: MockEmailStorage! - var privacyPreferences: PrivacySecurityPreferences! var duckPlayerPreferences: DuckPlayerPreferencesPersistor! + var coookiePopupProtectionPreferences: MockCookiePopupProtectionPreferencesPersistor! var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @@ -86,7 +86,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionVM = TabCollectionViewModel() emailStorage = MockEmailStorage() emailManager = EmailManager(storage: emailStorage) - privacyPreferences = PrivacySecurityPreferences.shared duckPlayerPreferences = DuckPlayerPreferencesPersistorMock() privacyConfigManager = MockPrivacyConfigurationManager() let config = MockPrivacyConfiguration() @@ -123,7 +122,6 @@ final class ContinueSetUpModelTests: XCTestCase { dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: messaging, privacyConfigurationManager: privacyConfigManager, @@ -138,7 +136,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionVM = nil emailManager = nil emailStorage = nil - privacyPreferences = nil vm = nil } @@ -162,14 +159,12 @@ final class ContinueSetUpModelTests: XCTestCase { capturingDefaultBrowserProvider.isDefault = true capturingDataImportProvider.didImport = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true - privacyPreferences.autoconsentEnabled = true vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: createMessaging() ) @@ -406,22 +401,6 @@ final class ContinueSetUpModelTests: XCTestCase { XCTAssertTrue(vm.visibleFeaturesMatrix[0].count <= vm.itemsPerRow) } - @MainActor func testWhenUserHasCookieConsentEnabledThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) - - privacyPreferences.autoconsentEnabled = true - vm = HomePage.Models.ContinueSetUpModel.fixture(privacyPreferences: privacyPreferences, appGroupUserDefaults: userDefaults) - - vm.shouldShowAllFeatures = true - - XCTAssertTrue(doTheyContainTheSameElements(matrix1: vm.visibleFeaturesMatrix, matrix2: expectedMatrix)) - - vm.shouldShowAllFeatures = false - - XCTAssertEqual(vm.visibleFeaturesMatrix.count, 1) - XCTAssertTrue(vm.visibleFeaturesMatrix[0].count <= vm.itemsPerRow) - } - @MainActor func testWhenAskedToPerformActionForDuckPlayerThenItOpensYoutubeVideo() { vm.performAction(for: .duckplayer) @@ -519,7 +498,6 @@ final class ContinueSetUpModelTests: XCTestCase { @MainActor func testThatWhenIfAllFeatureActiveThenVisibleMatrixIsEmpty() { capturingDefaultBrowserProvider.isDefault = true emailStorage.isEmailProtectionEnabled = true - privacyPreferences.autoconsentEnabled = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true capturingDataImportProvider.didImport = true userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0.rawValue) @@ -530,7 +508,6 @@ final class ContinueSetUpModelTests: XCTestCase { dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: createMessaging() ) @@ -627,7 +604,6 @@ extension HomePage.Models.ContinueSetUpModel { defaultBrowserProvider: DefaultBrowserProvider = CapturingDefaultBrowserProvider(), dataImportProvider: DataImportStatusProviding = CapturingDataImportProvider(), emailManager: EmailManager = EmailManager(storage: MockEmailStorage()), - privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, duckPlayerPreferences: DuckPlayerPreferencesPersistor = DuckPlayerPreferencesPersistorMock(), privacyConfig: MockPrivacyConfiguration = MockPrivacyConfiguration(), appGroupUserDefaults: UserDefaults, @@ -667,7 +643,6 @@ extension HomePage.Models.ContinueSetUpModel { dataImportProvider: dataImportProvider, tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: messaging, privacyConfigurationManager: manager, diff --git a/UnitTests/Preferences/AccessibilityPreferencesTests.swift b/UnitTests/Preferences/AccessibilityPreferencesTests.swift new file mode 100644 index 0000000000..819f2c6a0a --- /dev/null +++ b/UnitTests/Preferences/AccessibilityPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// AccessibilityPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockAccessibilityPreferencesPersistor: AccessibilityPreferencesPersistor { + var defaultPageZoom: CGFloat = DefaultZoomValue.percent100.rawValue +} + +class AccessibilityPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedDefaultPageZoom() { + let mockPersistor = MockAccessibilityPreferencesPersistor() + mockPersistor.defaultPageZoom = DefaultZoomValue.percent150.rawValue + let accessibilityPreferences = AccessibilityPreferences(persistor: mockPersistor) + + XCTAssertEqual(accessibilityPreferences.defaultPageZoom, DefaultZoomValue.percent150) + } + + func testWhenDefaultPageZoomUpdatedThenPersistorUpdates() { + let mockPersistor = MockAccessibilityPreferencesPersistor() + let accessibilityPreferences = AccessibilityPreferences(persistor: mockPersistor) + accessibilityPreferences.defaultPageZoom = .percent75 + + XCTAssertEqual(mockPersistor.defaultPageZoom, DefaultZoomValue.percent75.rawValue) + } + +} diff --git a/UnitTests/Preferences/AppearancePreferencesTests.swift b/UnitTests/Preferences/AppearancePreferencesTests.swift index 6ffbaec043..34e725fde4 100644 --- a/UnitTests/Preferences/AppearancePreferencesTests.swift +++ b/UnitTests/Preferences/AppearancePreferencesTests.swift @@ -25,20 +25,16 @@ struct AppearancePreferencesPersistorMock: AppearancePreferencesPersistor { var isContinueSetUpVisible: Bool var isRecentActivityVisible: Bool var showFullURL: Bool - var showAutocompleteSuggestions: Bool var currentThemeName: String var favoritesDisplayMode: String? - var defaultPageZoom: CGFloat var showBookmarksBar: Bool var bookmarksBarAppearance: BookmarksBarAppearance var homeButtonPosition: HomeButtonPosition init( showFullURL: Bool = false, - showAutocompleteSuggestions: Bool = true, currentThemeName: String = ThemeName.systemDefault.rawValue, favoritesDisplayMode: String? = FavoritesDisplayMode.displayNative(.desktop).description, - defaultPageZoom: CGFloat = DefaultZoomValue.percent100.rawValue, isContinueSetUpVisible: Bool = true, isFavoriteVisible: Bool = true, isRecentActivityVisible: Bool = true, @@ -47,10 +43,8 @@ struct AppearancePreferencesPersistorMock: AppearancePreferencesPersistor { homeButtonPosition: HomeButtonPosition = .right ) { self.showFullURL = showFullURL - self.showAutocompleteSuggestions = showAutocompleteSuggestions self.currentThemeName = currentThemeName self.favoritesDisplayMode = favoritesDisplayMode - self.defaultPageZoom = defaultPageZoom self.isContinueSetUpVisible = isContinueSetUpVisible self.isFavoriteVisible = isFavoriteVisible self.isRecentActivityVisible = isRecentActivityVisible @@ -66,10 +60,8 @@ final class AppearancePreferencesTests: XCTestCase { var model = AppearancePreferences( persistor: AppearancePreferencesPersistorMock( showFullURL: false, - showAutocompleteSuggestions: true, currentThemeName: ThemeName.systemDefault.rawValue, favoritesDisplayMode: FavoritesDisplayMode.displayNative(.desktop).description, - defaultPageZoom: DefaultZoomValue.percent100.rawValue, isContinueSetUpVisible: true, isFavoriteVisible: true, isRecentActivityVisible: true, @@ -78,10 +70,8 @@ final class AppearancePreferencesTests: XCTestCase { ) XCTAssertEqual(model.showFullURL, false) - XCTAssertEqual(model.showAutocompleteSuggestions, true) XCTAssertEqual(model.currentThemeName, ThemeName.systemDefault) XCTAssertEqual(model.favoritesDisplayMode, .displayNative(.desktop)) - XCTAssertEqual(model.defaultPageZoom, DefaultZoomValue.percent100) XCTAssertEqual(model.isFavoriteVisible, true) XCTAssertEqual(model.isContinueSetUpVisible, true) XCTAssertEqual(model.isRecentActivityVisible, true) @@ -90,10 +80,8 @@ final class AppearancePreferencesTests: XCTestCase { model = AppearancePreferences( persistor: AppearancePreferencesPersistorMock( showFullURL: true, - showAutocompleteSuggestions: false, currentThemeName: ThemeName.light.rawValue, favoritesDisplayMode: FavoritesDisplayMode.displayUnified(native: .desktop).description, - defaultPageZoom: DefaultZoomValue.percent50.rawValue, isContinueSetUpVisible: false, isFavoriteVisible: false, isRecentActivityVisible: false, @@ -101,10 +89,8 @@ final class AppearancePreferencesTests: XCTestCase { ) ) XCTAssertEqual(model.showFullURL, true) - XCTAssertEqual(model.showAutocompleteSuggestions, false) XCTAssertEqual(model.currentThemeName, ThemeName.light) XCTAssertEqual(model.favoritesDisplayMode, .displayUnified(native: .desktop)) - XCTAssertEqual(model.defaultPageZoom, DefaultZoomValue.percent50) XCTAssertEqual(model.isFavoriteVisible, false) XCTAssertEqual(model.isContinueSetUpVisible, false) XCTAssertEqual(model.isRecentActivityVisible, false) @@ -143,18 +129,6 @@ final class AppearancePreferencesTests: XCTestCase { XCTAssertEqual(NSApp.appearance?.name, ThemeName.systemDefault.appearance?.name) } - func testWhenZoomLevelChangedInAppearancePreferencesThenThePersisterAndUserDefaultsZoomValuesAreUpdated() { - UserDefaultsWrapper.clearAll() - let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! - let persister = AppearancePreferencesUserDefaultsPersistor() - let model = AppearancePreferences(persistor: persister) - model.defaultPageZoom = randomZoomLevel - - XCTAssertEqual(persister.defaultPageZoom, randomZoomLevel.rawValue) - let savedZoomValue = UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue).wrappedValue - XCTAssertEqual(savedZoomValue, randomZoomLevel.rawValue) - } - func testWhenNewTabPreferencesAreUpdatedThenPersistedValuesAreUpdated() throws { let model = AppearancePreferences(persistor: AppearancePreferencesPersistorMock()) diff --git a/UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift b/UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift new file mode 100644 index 0000000000..fa8cf471cc --- /dev/null +++ b/UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// CookiePopupProtectionPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockCookiePopupProtectionPreferencesPersistor: CookiePopupProtectionPreferencesPersistor { + var autoconsentEnabled: Bool = false +} + +class CookiePopupProtectionPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedAutoconsentSetting() { + let mockPersistor = MockCookiePopupProtectionPreferencesPersistor() + mockPersistor.autoconsentEnabled = true + let cookiePopupPreferences = CookiePopupProtectionPreferences(persistor: mockPersistor) + + XCTAssertTrue(cookiePopupPreferences.isAutoconsentEnabled) + } + + func testWhenIsAutoconsentEnabledUpdatedThenPersistorUpdates() { + let mockPersistor = MockCookiePopupProtectionPreferencesPersistor() + let cookiePopupPreferences = CookiePopupProtectionPreferences(persistor: mockPersistor) + cookiePopupPreferences.isAutoconsentEnabled = false + + XCTAssertFalse(mockPersistor.autoconsentEnabled) + } + +} diff --git a/UnitTests/Preferences/DataClearingPreferencesTests.swift b/UnitTests/Preferences/DataClearingPreferencesTests.swift new file mode 100644 index 0000000000..5563fb6e90 --- /dev/null +++ b/UnitTests/Preferences/DataClearingPreferencesTests.swift @@ -0,0 +1,43 @@ +// +// DataClearingPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockFireButtonPreferencesPersistor: FireButtonPreferencesPersistor { + var loginDetectionEnabled: Bool = false +} + +class DataClearingPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedLoginDetectionSetting() { + let mockPersistor = MockFireButtonPreferencesPersistor() + mockPersistor.loginDetectionEnabled = true + let dataClearingPreferences = DataClearingPreferences(persistor: mockPersistor) + + XCTAssertTrue(dataClearingPreferences.isLoginDetectionEnabled) + } + + func testWhenIsLoginDetectionEnabledUpdatedThenPersistorUpdates() { + let mockPersistor = MockFireButtonPreferencesPersistor() + let dataClearingPreferences = DataClearingPreferences(persistor: mockPersistor) + dataClearingPreferences.isLoginDetectionEnabled = true + + XCTAssertTrue(mockPersistor.loginDetectionEnabled) + } +} diff --git a/UnitTests/Preferences/PreferencesSidebarModelTests.swift b/UnitTests/Preferences/PreferencesSidebarModelTests.swift index cb9a8f0c60..09c31efdb0 100644 --- a/UnitTests/Preferences/PreferencesSidebarModelTests.swift +++ b/UnitTests/Preferences/PreferencesSidebarModelTests.swift @@ -40,7 +40,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } func testWhenInitializedThenFirstPaneInFirstSectionIsSelected() throws { - let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .downloads, .autofill])] + let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .autofill])] let model = PreferencesSidebarModel(loadSections: sections) XCTAssertEqual(model.selectedPane, .appearance) @@ -72,7 +72,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } func testWhenSelectPaneIsCalledWithNonexistentPaneThenItHasNoEffect() throws { - let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .downloads])] + let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .autofill])] let model = PreferencesSidebarModel(loadSections: sections) model.selectPane(.general) @@ -80,7 +80,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } func testWhenSelectedTabIndexIsChangedThenSelectedPaneIsNotAffected() throws { - let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.general, .appearance, .downloads, .autofill])] + let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.general, .appearance, .autofill])] let tabs: [Tab.TabContent] = [.anySettingsPane, .bookmarks] let model = PreferencesSidebarModel(loadSections: sections, tabSwitcherTabs: tabs) diff --git a/UnitTests/Preferences/PrivacyProtectionStatusTests.swift b/UnitTests/Preferences/PrivacyProtectionStatusTests.swift new file mode 100644 index 0000000000..4ec5f7af1f --- /dev/null +++ b/UnitTests/Preferences/PrivacyProtectionStatusTests.swift @@ -0,0 +1,67 @@ +// +// PrivacyProtectionStatusTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +class PrivacyProtectionStatusTests: XCTestCase { + + var cancellables: Set = [] + + override func tearDown() { + super.tearDown() + cancellables.removeAll() + } + + func testWhenStatusUpdatedFromPublisherThenReflectsNewStatus() { + // Example of how you might test updates from a publisher + let subject = PassthroughSubject() + let status = PrivacyProtectionStatus(statusPublisher: subject) { isOn in + return isOn ? .on : .off + } + + // Initial value should be nil + XCTAssertNil(status.status) + + let expectation = XCTestExpectation(description: "Status updates to .on") + status.$status.sink { newStatus in + if newStatus == .on { + expectation.fulfill() + } + }.store(in: &cancellables) + + subject.send(true) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenInitializedWithStaticStatusIndicatorThenSetsStatusCorrectly() { + // Test initialization with a static status indicator + let status = PrivacyProtectionStatus(statusIndicator: .on) + + XCTAssertEqual(status.status, .on) + } + + func testWhenInitializedWithoutStatusThenStatusIsNil() { + // Test initialization without a status + let status = PrivacyProtectionStatus() + + XCTAssertNil(status.status) + } +} diff --git a/UnitTests/Preferences/SearchPreferencesTests.swift b/UnitTests/Preferences/SearchPreferencesTests.swift new file mode 100644 index 0000000000..60f5e3a87c --- /dev/null +++ b/UnitTests/Preferences/SearchPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// SearchPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockSearchPreferencesPersistor: SearchPreferencesPersistor { + var showAutocompleteSuggestions: Bool = false +} + +class SearchPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedValues() { + let mockPersistor = MockSearchPreferencesPersistor() + mockPersistor.showAutocompleteSuggestions = true + let searchPreferences = SearchPreferences(persistor: mockPersistor) + + XCTAssertTrue(searchPreferences.showAutocompleteSuggestions) + } + + func testWhenShowAutocompleteSuggestionsUpdatedThenPersistorUpdates() { + let mockPersistor = MockSearchPreferencesPersistor() + let searchPreferences = SearchPreferences(persistor: mockPersistor) + searchPreferences.showAutocompleteSuggestions = true + + XCTAssertTrue(mockPersistor.showAutocompleteSuggestions) + } + +} diff --git a/UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift b/UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift new file mode 100644 index 0000000000..428634e4e8 --- /dev/null +++ b/UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// WebTrackingProtectionPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockWebTrackingProtectionPreferencesPersistor: WebTrackingProtectionPreferencesPersistor { + var gpcEnabled: Bool = false +} + +class WebTrackingProtectionPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedGPCSetting() { + let mockPersistor = MockWebTrackingProtectionPreferencesPersistor() + mockPersistor.gpcEnabled = true + let webTrackingPreferences = WebTrackingProtectionPreferences(persistor: mockPersistor) + + XCTAssertTrue(webTrackingPreferences.isGPCEnabled) + } + + func testWhenIsGPCEnabledUpdatedThenPersistorUpdates() { + let mockPersistor = MockWebTrackingProtectionPreferencesPersistor() + let webTrackingPreferences = WebTrackingProtectionPreferences(persistor: mockPersistor) + webTrackingPreferences.isGPCEnabled = true + + XCTAssertTrue(mockPersistor.gpcEnabled) + } + +} diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 7dd1ce9b73..9dbaa4a0fd 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -214,7 +214,7 @@ final class TabViewModelTests: XCTestCase { @MainActor func testThatDefaultValueForTabsWebViewIsOne() { UserDefaultsWrapper.clearAll() - let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences()) + let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences(), accessibilityPreferences: AccessibilityPreferences()) XCTAssertEqual(tabVM.tab.webView.zoomLevel, DefaultZoomValue.percent100) } @@ -224,7 +224,7 @@ final class TabViewModelTests: XCTestCase { UserDefaultsWrapper.clearAll() let tabVM = TabViewModel(tab: Tab()) let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! - AppearancePreferences.shared.defaultPageZoom = randomZoomLevel + AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) } @@ -233,7 +233,7 @@ final class TabViewModelTests: XCTestCase { func testWhenAppearancePreferencesZoomLevelIsSetAndANewTabIsOpenThenItsWebViewHasTheLatestValueOfZoomLevel() { UserDefaultsWrapper.clearAll() let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! - AppearancePreferences.shared.defaultPageZoom = randomZoomLevel + AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences()) diff --git a/UnitTests/Tab/WebViewTests.swift b/UnitTests/Tab/WebViewTests.swift index df9920004e..aedaeccc0d 100644 --- a/UnitTests/Tab/WebViewTests.swift +++ b/UnitTests/Tab/WebViewTests.swift @@ -134,7 +134,7 @@ final class WebViewTests: XCTestCase { let tabVM = TabViewModel(tab: Tab()) let randomZoomLevel = DefaultZoomValue.percent300 // Select Default zoom - AppearancePreferences.shared.defaultPageZoom = randomZoomLevel + AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel // Zooming out tabVM.tab.webView.zoomOut() @@ -147,7 +147,7 @@ final class WebViewTests: XCTestCase { XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) // Set new default zoom - AppearancePreferences.shared.defaultPageZoom = .percent75 + AccessibilityPreferences.shared.defaultPageZoom = .percent75 XCTAssertEqual(tabVM.tab.webView.zoomLevel, .percent75) } diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift index 091bd0d2b1..875c7746a4 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift @@ -147,9 +147,9 @@ extension TabCollectionViewModelTests { tabCollectionViewModel.tabCollection.append(tab: .init(content: .settings(pane: .appearance))) tabCollectionViewModel.tabCollection.append(tab: .init(content: .newtab)) - XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .privacy))) + XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .general))) XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) - XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .privacy)) + XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .general)) } func test_WithoutPinnedTabsManager_WhenPreferencesTabIsPresentThenOpeningPreferencesWithAnyPaneDoesNotUpdatePaneOnExistingTab() { diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift index c27417fba0..efc39feac0 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift @@ -138,9 +138,9 @@ final class TabCollectionViewModelTests: XCTestCase { tabCollectionViewModel.tabCollection.append(tab: .init(content: .settings(pane: .appearance))) tabCollectionViewModel.tabCollection.append(tab: .init(content: .newtab)) - XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .privacy))) + XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .general))) XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) - XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .privacy)) + XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .general)) } func testWhenPreferencesTabIsPresentThenOpeningPreferencesWithAnyPaneDoesNotUpdatePaneOnExistingTab() { From cf1e711f7a47deab1d15381bd4568f615a365b62 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 19 Mar 2024 19:52:54 +0100 Subject: [PATCH 14/17] Display error messaging for cancelled subscriptions (#2394) Task/Issue URL: https://app.asana.com/0/72649045549333/1205438842252963/f Description: When we detect that entitlements have expire AND Netp is enabled: Disable NetP Show revoked notification and make sure that Netp disabled notification is NOT shown. On opening the status view we show the revoked dialog. When we get 403 response from /register (or any controller APIs), we handle the expired entitlement (refer to first bullet point in this section). (Handled by the iOS equivalent project When rekey is attempted with invalid entitlements, we will get 403. (Handled by the iOS equivalent project If user attempts to enable Netp with invalid entitlements, we will get 403. (Handled by the iOS equivalent project We do a periodic check every 20 minutes calling the subscription API to get entitlements (Handled by the NetworkProtectionEntitlementsMonitor implemented in the iOS equivalent project ) If check succeeds and user has entitlement, we do nothing. If check succeeds and user has no entitlement, we handle the expired entitlement (refer to first bullet point in this section). If check fails (no internet, BE failure, tunnel failure), we do nothing. If Netp is disabled, we stop this check. On top of this, we also added an additional check on the Settings page. To correctly show the Netp entry, we check for entitlement through subscription API whenever the Settings page is foregrounded. If check succeeds and user has entitlement, we show the Netp settings entry. If check succeeds and user has no entitlement, we handle the expired entitlement (refer to first bullet point in this section) and also hide Netp settings entry. If check fails (no internet, be failure, tunnel failure), we retain whatever the previous visibility of the Settings entry if available, else hide entry. --- .../NetworkProtection/DuckDuckGoVPN.xcconfig | 8 +- .../NetworkProtectionSystemExtension.xcconfig | 8 +- DuckDuckGo.xcodeproj/project.pbxproj | 31 ++++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/Application/AppDelegate.swift | 3 +- DuckDuckGo/Application/URLEventHandler.swift | 6 +- .../MainWindow/MainViewController.swift | 2 +- .../View/NavigationBarViewController.swift | 16 ++-- .../AppLauncher.swift | 3 + ...rkProtection+ConvenienceInitializers.swift | 29 +++++++- .../NetworkProtectionAppEvents.swift | 2 + ...etworkProtectionNavBarPopoverManager.swift | 30 +++++--- .../NetworkProtectionTunnelController.swift | 27 ++++++- .../VPNLocation/VPNLocationViewModel.swift | 12 --- ...rkProtectionSubscriptionEventHandler.swift | 43 ++++++++--- ...rkProtectionUNNotificationsPresenter.swift | 12 ++- ...UserText+NetworkProtectionExtensions.swift | 2 + .../MacPacketTunnelProvider.swift | 34 +++++++-- ...ore+SubscriptionTokenKeychainStorage.swift | 45 +++++++++++ .../Model/PreferencesSidebarModel.swift | 21 +++++- .../AccountManagerExtension.swift | 1 + .../SubscriptionFeatureAvailability.swift | 29 +++++--- .../NetworkProtectionFeatureDisabler.swift | 13 +--- .../NetworkProtectionFeatureVisibility.swift | 5 ++ .../DuckDuckGoNotificationsAppDelegate.swift | 12 +++ DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 52 ++++++++++++- ...tion+VPNAgentConvenienceInitializers.swift | 33 +++++++++ DuckDuckGoVPN/NetworkProtectionBouncer.swift | 26 ++++++- DuckDuckGoVPN/VPNUninstaller.swift | 60 +++++++++++++++ LocalPackages/LoginItems/Package.swift | 1 + .../Sources/LoginItems/LoginItem.swift | 4 + .../AppLaunching/AppLaunching.swift | 1 + ...serDefault+ShowVPNUninstalledMessage.swift | 44 +++++++++++ .../UserDefault+VPNEnabledViaWaitlist.swift | 44 +++++++++++ ...NetworkProtectionExpiredEntitlements.swift | 44 +++++++++++ .../UserText+NetworkProtectionUI.swift | 7 ++ .../Menu/StatusBarMenu.swift | 9 ++- .../NetworkProtectionPopover.swift | 8 +- .../NetworkProtectionStatusView.swift | 9 ++- .../NetworkProtectionStatusViewModel.swift | 37 +++++++++- .../SubscriptionExpiredView.swift | 74 +++++++++++++++++++ .../NetworkProtectionStatusBarMenuTests.swift | 8 +- LocalPackages/SyncUI/Package.swift | 1 + .../SystemExtensionManager/Package.swift | 1 + ...rotectionAgentNotificationsPresenter.swift | 8 +- 45 files changed, 756 insertions(+), 113 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift create mode 100644 DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift create mode 100644 DuckDuckGoVPN/VPNUninstaller.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift diff --git a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig index 662be4ddeb..b3b7f7ef35 100644 --- a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig +++ b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig @@ -49,10 +49,10 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS NetP VPN App - Review (XPC) PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = macOS NetP VPN App - Release (XPC) -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION SWIFT_OBJC_BRIDGING_HEADER = SKIP_INSTALL = YES diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig index 9cc1c7ae28..325f9024b7 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig @@ -31,10 +31,10 @@ INFOPLIST_FILE = NetworkProtectionSystemExtension/Info.plist INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. INFOPLIST_KEY_NSSystemExtensionUsageDescription = DuckDuckGo VPN -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(SYSEX_BUNDLE_ID) PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 66a3b706f0..1596da5917 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2278,8 +2278,6 @@ 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; - 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */; }; - 7BA7CC5B2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */; }; 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA7CC5E2AD1210C0042E5CE /* Networking */; }; @@ -3223,6 +3221,11 @@ EE0629762B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */; }; EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE2F9C5A2B90F2FF00D45FC9 /* Subscription */; }; EE339228291BDEFD009F62C1 /* JSAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE339227291BDEFD009F62C1 /* JSAlertController.swift */; }; + EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; + EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; + EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE66418E2B9B1BD1005BCD17 /* Subscription */; }; EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666712B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; @@ -3258,6 +3261,8 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; + EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; + EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; @@ -4637,6 +4642,8 @@ EAFAD6C92728BD1200F9DF00 /* clickToLoad.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = clickToLoad.js; sourceTree = ""; }; EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerExtension.swift; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; + EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; + EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; @@ -4647,6 +4654,7 @@ EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItemModel.swift; sourceTree = ""; }; EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItem.swift; sourceTree = ""; }; EECE10E429DD77E60044D027 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; + EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+VPNAgentConvenienceInitializers.swift"; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; @@ -4734,6 +4742,7 @@ buildActionMask = 2147483647; files = ( 37269F012B332FC8005E8E46 /* Common in Frameworks */, + EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, @@ -5761,6 +5770,7 @@ 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, + EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6414,9 +6424,11 @@ 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, + EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, + EE34245D2BA0853900173B1B /* VPNUninstaller.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -8800,6 +8812,7 @@ EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, + EE66418E2B9B1BD1005BCD17 /* Subscription */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -11067,6 +11080,7 @@ files = ( 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, + EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, @@ -11090,10 +11104,10 @@ files = ( B6F92BA22A691580002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */, - 7BA7CC5B2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, + EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, @@ -11104,6 +11118,7 @@ EE0629742B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, + EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, @@ -11127,9 +11142,9 @@ 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, + EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, - 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -11138,6 +11153,7 @@ EE0629752B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */, 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, + EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -11175,6 +11191,7 @@ 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 7B25856C2BA2F2D000D49F79 /* AppLauncher.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, @@ -14074,7 +14091,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.1.0; + version = 126.2.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -14774,6 +14791,10 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; + EE66418E2B9B1BD1005BCD17 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + productName = Subscription; + }; EE7295E22A545B9A008C0991 /* NetworkProtection */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7a11e2163c..7d241ed988 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "f4894b9c00dd7514c66d6b929c12315e0cd9c151", - "version" : "126.1.0" + "branch" : "graeme/expired-entitlements-stuff", + "revision" : "925d0dd50e47f38c7fe922622002e8961569bc32" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index d0aa5916c6..e4f938425a 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -82,7 +82,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel var privacyDashboardWindow: NSWindow? #if NETWORK_PROTECTION && SUBSCRIPTION - private let networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() + // Needs to be lazy as indirectly depends on AppDelegate + private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() #endif #if DBP && SUBSCRIPTION diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index d73a1ea414..e0e752ce6f 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -143,8 +143,12 @@ final class URLEventHandler { case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() - case AppLaunchCommand.moveAppToApplications.launchURL: +#if SUBSCRIPTION + case AppLaunchCommand.showPrivacyPro.launchURL: + WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) +#endif #if !APPSTORE && !DEBUG + case AppLaunchCommand.moveAppToApplications.launchURL: // this should be run after NSApplication.shared is set PFMoveToApplicationsFolderIfNecessary(false) #endif diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 8dae1a9c7f..e5950d0231 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -75,7 +75,7 @@ final class MainViewController: NSViewController { let ipcClient = TunnelControllerIPCClient(machServiceName: vpnBundleID) ipcClient.register() - return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient) + return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient, networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabler()) }() let networkProtectionStatusReporter: NetworkProtectionStatusReporter = { var connectivityIssuesObserver: ConnectivityIssueObserver! diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index aa050d3a98..fbe69492b5 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -438,14 +438,20 @@ final class NavigationBarViewController: NSViewController { object: nil) #if NETWORK_PROTECTION - NotificationCenter.default.addObserver(self, - selector: #selector(showVPNUninstalledFeedback(_:)), - name: NetworkProtectionFeatureDisabler.vpnUninstalledNotificationName, - object: nil) + UserDefaults.netP + .publisher(for: \.networkProtectionShouldShowVPNUninstalledMessage) + .receive(on: DispatchQueue.main) + .sink { [weak self] shouldShowUninstalledMessage in + if shouldShowUninstalledMessage { + self?.showVPNUninstalledFeedback() + UserDefaults.netP.networkProtectionShouldShowVPNUninstalledMessage = false + } + } + .store(in: &cancellables) #endif } - @objc private func showVPNUninstalledFeedback(_ sender: Notification) { + @objc private func showVPNUninstalledFeedback() { guard view.window?.isKeyWindow == true else { return } DispatchQueue.main.async { diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift index 0ec2416135..b85ed68d8b 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift @@ -36,6 +36,7 @@ extension AppLaunchCommand { case .showVPNLocations: return "showVPNLocations" case .enableOnDemand: return "enableOnDemand" case .moveAppToApplications: return "moveAppToApplications" + case .showPrivacyPro: return "showPrivacyPro" } } } @@ -97,6 +98,8 @@ extension AppLaunchCommand { return "networkprotection://show-settings/locations" case .moveAppToApplications: return "networkprotection://move-app-to-applications" + case .showPrivacyPro: + return "networkprotection://show-privacy-pro" default: return nil } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index fcdb50ff75..8eed556fac 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -22,6 +22,10 @@ import Foundation import NetworkProtection import Common +#if SUBSCRIPTION +import Subscription +#endif + extension NetworkProtectionDeviceManager { static func create() -> NetworkProtectionDeviceManager { @@ -32,7 +36,7 @@ extension NetworkProtectionDeviceManager { tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) } } @@ -42,16 +46,21 @@ extension NetworkProtectionCodeRedemptionCoordinator { self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) } } extension NetworkProtectionKeychainTokenStore { convenience init() { +#if SUBSCRIPTION + let accessTokenProvider: () -> String? = { AccountManager().accessToken } +#else + let accessTokenProvider: () -> String? = { return nil } +#endif self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false, - accessTokenProvider: { nil }) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable(), + accessTokenProvider: accessTokenProvider) } } @@ -62,4 +71,16 @@ extension NetworkProtectionKeychainKeyStore { } } +extension NetworkProtectionLocationListCompositeRepository { + convenience init() { + let settings = VPNSettings(defaults: .netP) + self.init( + environment: settings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore(), + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable() + ) + } +} + #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index ce62ed2734..23d20a89d2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -72,9 +72,11 @@ final class NetworkProtectionAppEvents { /// func applicationDidBecomeActive() { guard featureVisibility.isNetworkProtectionVisible() else { + UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = false featureVisibility.disableForAllUsers() return } + UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = true } private func restartNetworkProtectionIfVersionChanged(using loginItemsManager: LoginItemsManager) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 16853163e8..331892d0f5 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -24,6 +24,10 @@ import NetworkProtection import NetworkProtectionIPC import NetworkProtectionUI +#if SUBSCRIPTION +import Subscription +#endif + #if NETWORK_PROTECTION protocol NetworkProtectionIPCClient { @@ -34,6 +38,7 @@ protocol NetworkProtectionIPCClient { func start() func stop() } + extension TunnelControllerIPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } @@ -43,9 +48,12 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { private var networkProtectionPopover: NetworkProtectionPopover? let ipcClient: NetworkProtectionIPCClient + let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling - init(ipcClient: NetworkProtectionIPCClient) { + init(ipcClient: TunnelControllerIPCClient, + networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling) { self.ipcClient = ipcClient + self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler } var isShown: Bool { @@ -56,15 +64,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { #endif } - private func show(_ popover: NSPopover, positionedBelow view: NSView) { - view.isHidden = false - - popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) - } - // swiftlint:disable:next function_body_length func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { - let popover = networkProtectionPopover ?? { let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) @@ -117,8 +118,11 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { } }, agentLoginItem: LoginItem.vpnMenu, - isMenuBarStatusView: false - ) + isMenuBarStatusView: false, + userDefaults: .netP, + uninstallHandler: { [weak self] in + _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) + }) popover.delegate = delegate networkProtectionPopover = popover @@ -128,6 +132,12 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { show(popover, positionedBelow: view) } + private func show(_ popover: NSPopover, positionedBelow view: NSView) { + view.isHidden = false + + popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) + } + func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { if let networkProtectionPopover, networkProtectionPopover.isShown { networkProtectionPopover.close() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 619da111ff..8ecf83100d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -34,6 +34,10 @@ import SystemExtensionManager import SystemExtensions #endif +#if SUBSCRIPTION +import Subscription +#endif + typealias NetworkProtectionStatusChangeHandler = (NetworkProtection.ConnectionStatus) -> Void typealias NetworkProtectionConfigChangeHandler = () -> Void @@ -72,6 +76,12 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Auth token store private let tokenStore: NetworkProtectionTokenStore +#if SUBSCRIPTION + // MARK: - Subscriptions + + private let accountManager = AccountManager() +#endif + // MARK: - Debug Options Support private let networkExtensionBundleID: String @@ -538,7 +548,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr var options = [String: NSObject]() options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString - guard let authToken = try tokenStore.fetchToken() as NSString? else { + guard let authToken = try fetchAuthToken() else { throw StartError.noAuthToken } options[NetworkProtectionOptionKey.authToken] = authToken @@ -738,6 +748,21 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr throw TunnelFailureError(errorDescription: errorMessage.value) } } + + private func fetchAuthToken() throws -> NSString? { +#if SUBSCRIPTION + if let accessToken = accountManager.accessToken { + os_log(.error, log: .networkProtection, "🟢 TunnelController found token: %{public}d", accessToken) + return Self.adaptAccessTokenForVPN(accessToken) as NSString? + } +#endif + os_log(.error, log: .networkProtection, "🔴 TunnelController found no token :(") + return try tokenStore.fetchToken() as NSString? + } + + private static func adaptAccessTokenForVPN(_ token: String) -> String { + "ddg:\(token)" + } } #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index fb1b74c2ff..24dfd9c4ab 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -185,18 +185,6 @@ extension VPNCityItemModel { } } -extension NetworkProtectionLocationListCompositeRepository { - convenience init() { - let settings = VPNSettings(defaults: .netP) - self.init( - environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false - ) - } -} - extension VPNLocationViewModel { convenience init() { let locationListRepository = NetworkProtectionLocationListCompositeRepository() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 7ad69d346b..9f7287b8a7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -21,6 +21,7 @@ import Foundation import Subscription import NetworkProtection +import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { @@ -28,40 +29,60 @@ final class NetworkProtectionSubscriptionEventHandler { private let networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling + private let userDefaults: UserDefaults init(accountManager: AccountManaging = AccountManager(), networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming = NetworkProtectionCodeRedemptionCoordinator(), networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), - networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler()) { + networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + userDefaults: UserDefaults = .netP) { self.accountManager = accountManager self.networkProtectionRedemptionCoordinator = networkProtectionRedemptionCoordinator self.networkProtectionTokenStorage = networkProtectionTokenStorage self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler + self.userDefaults = userDefaults + } + + private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() + + private func setUpEntitlementMonitoring() { + guard AccountManager().isUserAuthenticated else { return } + let entitlementsCheck = { + await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } + + Task { + await entitlementMonitor.start(entitlementCheck: entitlementsCheck) { result in + switch result { + case .validEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = false + case .invalidEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = true + case .error: + break + } + } + } } func registerForSubscriptionAccountManagerEvents() { NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) + setUpEntitlementMonitoring() } @objc private func handleAccountDidSignIn() { - guard let token = accountManager.accessToken else { + guard accountManager.accessToken != nil else { assertionFailure("[NetP Subscription] AccountManager signed in but token could not be retrieved") return } - - Task { - do { - try await networkProtectionRedemptionCoordinator.exchange(accessToken: token) - print("[NetP Subscription] Exchanged access token for auth token successfully") - } catch { - print("[NetP Subscription] Failed to exchange access token for auth token: \(error)") - } - } + userDefaults.networkProtectionEntitlementsExpired = false + setUpEntitlementMonitoring() } @objc private func handleAccountDidSignOut() { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") + userDefaults.networkProtectionEntitlementsExpired = true Task { await networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index eefd12cff5..3977fccef5 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -47,6 +47,7 @@ extension UNNotificationCategory { /// This class takes care of requesting the presentation of notifications using UNNotificationCenter /// final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtectionNotificationsPresenter { + private static let threadIdentifier = "com.duckduckgo.NetworkProtectionNotificationsManager.threadIdentifier" private let appLauncher: AppLauncher @@ -134,6 +135,12 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.superseded, content) } + func showEntitlementNotification() { + let content = notificationContent(title: UserText.networkProtectionEntitlementExpiredNotificationTitle, + subtitle: UserText.networkProtectionEntitlementExpiredNotificationBody) + showNotification(.expiredEntitlement, content) + } + func showTestNotification() { // These strings are deliberately hardcoded as we don't want them localized, they're only for debugging: let content = notificationContent(title: "Test notification", @@ -141,10 +148,6 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.test, content) } - func showEntitlementNotification() { - // todo - } - private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) { let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none) @@ -166,6 +169,7 @@ public enum NetworkProtectionNotificationIdentifier: String { case reconnecting = "network-protection.notification.reconnecting" case connected = "network-protection.notification.connected" case superseded = "network-protection.notification.superseded" + case expiredEntitlement = "network-protection.notification.expired-entitlement" case test = "network-protection.notification.test" } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift index 607fb081be..ecc8f96073 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift @@ -43,4 +43,6 @@ final class UserText { static let networkProtectionSupersededNotificationSubtitle = NSLocalizedString("network.protection.superceded.notification.subtitle", value: "Another VPN app on your Mac may have disabled it.", comment: "The subtitle of the notification shown when VPN connection is replaced by another app VPN connection taking over") static let networkProtectionSupersededReconnectActionTitle = NSLocalizedString("network.protection.superceded.action.reconnect.title", value: "Reconnect", comment: "The title of the `Reconnect` notification action button shown when VPN connection is replaced by another app VPN connection taking over") + static let networkProtectionEntitlementExpiredNotificationTitle = NSLocalizedString("network.protection.entitlement.expired.notification.title", value: "VPN disconnected", comment: "The title of the notification when Privacy Pro subscription expired") + static let networkProtectionEntitlementExpiredNotificationBody = NSLocalizedString("network.protection.entitlement.expired.notification.body", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "The body of the notification when Privacy Pro subscription expired") } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index da8d81fc24..acb06735b3 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -24,6 +24,10 @@ import NetworkExtension import Networking import PixelKit +#if SUBSCRIPTION +import Subscription +#endif + final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Additional Status Info @@ -265,6 +269,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization @objc public init() { + let isSubscriptionEnabled = false + #if NETP_SYSTEM_EXTENSION let defaults = UserDefaults.standard #else @@ -274,12 +280,26 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, - serviceName: Self.tokenServiceName, - errorEvents: debugEvents, - isSubscriptionEnabled: false, - accessTokenProvider: { nil }) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, + serviceName: Self.tokenServiceName, + errorEvents: debugEvents, + isSubscriptionEnabled: isSubscriptionEnabled, + accessTokenProvider: { nil } + ) +#if SUBSCRIPTION + + let accountManager = AccountManager( + accessTokenStorage: tokenStore, + entitlementsCache: UserDefaultsCache<[Entitlement]>(key: UserDefaultsCacheKey.subscriptionEntitlements) + ) + SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging + let entitlementsCheck = { + await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } +#else + let entitlementsCheck: (() async -> Result)? = nil +#endif super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, @@ -290,8 +310,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: defaults, - isSubscriptionEnabled: false, - entitlementCheck: nil) + isSubscriptionEnabled: isSubscriptionEnabled, + entitlementCheck: entitlementsCheck) setupPixels() observeServerChanges() diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift new file mode 100644 index 0000000000..2987ab2ff9 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift @@ -0,0 +1,45 @@ +// +// NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if SUBSCRIPTION + +import Foundation +import Subscription +import NetworkProtection +import Common + +extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStorage { + public func store(accessToken: String) throws { + try store(accessToken) + } + + public func getAccessToken() throws -> String? { + guard var token = try fetchToken() else { return nil } + if token.hasPrefix("ddg:") { + token = token.replacingOccurrences(of: "ddg:", with: "") + } + os_log("🔵 Wrapper successfully fetched token %{token}@", log: .networkProtection, type: .info, token) + return token + } + + public func removeAccessToken() throws { + try deleteToken() + } +} + +#endif diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 97c5a06a42..18cbf84cda 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -21,6 +21,10 @@ import Combine import DDGSync import SwiftUI +#if SUBSCRIPTION +import Subscription +#endif + final class PreferencesSidebarModel: ObservableObject { let tabSwitcherTabs: [Tab.TabContent] @@ -75,10 +79,13 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - includeDuckPlayer: Bool + includeDuckPlayer: Bool, + userDefaults: UserDefaults = .netP ) { let loadSections = { -#if NETWORK_PROTECTION +#if SUBSCRIPTION + let includingVPN = !userDefaults.networkProtectionEntitlementsExpired && DefaultNetworkProtectionVisibility().isOnboarded +#elseif NETWORK_PROTECTION let includingVPN = DefaultNetworkProtectionVisibility().isOnboarded #else let includingVPN = false @@ -113,6 +120,16 @@ final class PreferencesSidebarModel: ObservableObject { self.refreshSections() } .store(in: &cancellables) + + UserDefaults.netP.publisher(for: \.networkProtectionEntitlementsExpired) + .receive(on: DispatchQueue.main) + .sink { [weak self] entitlementsExpired in + guard let self else { return } + if !entitlementsExpired && self.selectedPane == .vpn { + self.selectedPane = .general + } + self.refreshSections() + }.store(in: &cancellables) } #endif diff --git a/DuckDuckGo/Subscription/AccountManagerExtension.swift b/DuckDuckGo/Subscription/AccountManagerExtension.swift index c1758fb2e5..5f30dad322 100644 --- a/DuckDuckGo/Subscription/AccountManagerExtension.swift +++ b/DuckDuckGo/Subscription/AccountManagerExtension.swift @@ -17,6 +17,7 @@ // #if SUBSCRIPTION +import Foundation import Subscription public extension AccountManager { diff --git a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift index 6599bd0ee9..9090ffad02 100644 --- a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift +++ b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift @@ -24,6 +24,7 @@ import Subscription #if NETWORK_PROTECTION import NetworkProtection +import BrowserServicesKit #endif protocol SubscriptionFeatureAvailability { @@ -36,8 +37,8 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { #if SUBSCRIPTION_OVERRIDE_ENABLED return true #elseif SUBSCRIPTION - print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isVPNActivated: [\(isVPNActivated)] | isDBPActivated: [\(isDBPActivated)]") - return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isVPNActivated && !isDBPActivated) + print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isDBPActivated: [\(isDBPActivated)]") + return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isDBPActivated) #else return false #endif @@ -48,15 +49,7 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { } private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - - private var isVPNActivated: Bool { -#if NETWORK_PROTECTION - return NetworkProtectionKeychainTokenStore().isFeatureActivated -#else - return false -#endif + Self.internalUserDecider.isInternalUser } private var isDBPActivated: Bool { @@ -66,4 +59,18 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { return false #endif } + + private static var internalUserDecider: InternalUserDecider = { + let keyStore = EncryptionKeyStore() + let fileStore: FileStore + do { + let encryptionKey = NSApplication.runType.requiresEnvironment ? try keyStore.readKey() : nil + fileStore = EncryptedFileStore(encryptionKey: encryptionKey) + } catch { + fileStore = EncryptedFileStore() + } + + let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) + return DefaultInternalUserDecider(store: internalUserDeciderStore) + }() } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index f33b15c8c7..dd5572e94c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -33,8 +33,6 @@ protocol NetworkProtectionFeatureDisabling { } final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling { - static let vpnUninstalledNotificationName = NSNotification.Name(rawValue: "com.duckduckgo.NetworkProtection.uninstalled") - private let log: OSLog private let loginItemsManager: LoginItemsManager private let pinningManager: LocalPinningManager @@ -91,7 +89,7 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling } unpinNetworkProtection() - postVPNUninstalledNotification() + notifyVPNUninstalled() return true } @@ -126,14 +124,11 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling settings.resetToDefaults() } - private func postVPNUninstalledNotification() { - Task { @MainActor in + private func notifyVPNUninstalled() { // Wait a bit since the NetP button is likely being hidden + Task { try? await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) - - NotificationCenter.default.post( - name: Self.vpnUninstalledNotificationName, - object: nil) + userDefaults.networkProtectionShouldShowVPNUninstalledMessage = true } } } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 6e9386c491..08d52679a3 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -24,6 +24,7 @@ import Common import NetworkExtension import NetworkProtection import NetworkProtectionUI +import LoginItems protocol NetworkProtectionFeatureVisibility { func isNetworkProtectionVisible() -> Bool @@ -72,11 +73,15 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// Returns whether the VPN should be uninstalled automatically. /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. func shouldUninstallAutomatically() -> Bool { +#if SUBSCRIPTION + return defaults.networkProtectionEntitlementsExpired && LoginItem.vpnMenu.status.isInstalled +#else let waitlistAccessEnded = isWaitlistUser && !waitlistIsOngoing let isNotEasterEggUser = !isEasterEggUser let isOnboarded = UserDefaults.netP.networkProtectionOnboardingStatus != .default return isNotEasterEggUser && waitlistAccessEnded && isOnboarded +#endif } /// Whether the user is fully onboarded diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index bd226a82c6..41677dc8f1 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -111,6 +111,12 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate os_log("Got notification: listener started") self?.notificationsPresenter.requestAuthorization() }.store(in: &cancellables) + + distributedNotificationCenter.publisher(for: .showExpiredEntitlementNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.showEntitlementNotification() + }.store(in: &cancellables) } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -139,6 +145,12 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate notificationsPresenter.showSupersededNotification() } + func showEntitlementNotification() { + os_log("Presenting Entitlements notification", log: .networkProtection, type: .info) + + notificationsPresenter.showEntitlementNotification() + } + func showTestNotification() { os_log("Presenting test notification", log: .networkProtection, type: .info) notificationsPresenter.showTestNotification() diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 4ca0fe2aa5..c44f70bbd7 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -47,6 +47,7 @@ final class DuckDuckGoVPNApplication: NSApplication { super.init() self.delegate = _delegate + #if DEBUG && SUBSCRIPTION let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) @@ -144,7 +145,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { }() private func handleControllerEvent(_ event: TransparentProxyController.Event) { - PixelKit.fire(event) + } @MainActor @@ -200,6 +201,10 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { VPNAppEventsHandler(tunnelController: tunnelController) }() + private lazy var vpnUninstaller: VPNUninstaller = { + VPNUninstaller(networkExtensionController: networkExtensionController, vpnConfigurationManager: VPNConfigurationManager()) + }() + /// The status bar NetworkProtection menu /// /// For some reason the App will crash if this is initialized right away, which is why it was changed to be lazy. @@ -249,12 +254,21 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ] }, agentLoginItem: nil, - isMenuBarStatusView: true) + isMenuBarStatusView: true, + userDefaults: .netP, + uninstallHandler: { [weak self] in + guard let self else { return } + await self.vpnUninstaller.uninstall(includingSystemExtension: true) + } + ) } @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) +#if SUBSCRIPTION + SubscriptionPurchaseEnvironment.currentServiceEnvironment = tunnelSettings.selectedEnvironment == .production ? .production : .staging +#endif os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog, type: .info) @@ -308,6 +322,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { let launchedOnStartup = launchInformation.wasLaunchedByStartup launchInformation.update() + setUpSubscriptionMonitoring() + if launchedOnStartup { Task { let isConnected = await tunnelController.isConnected @@ -338,6 +354,38 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { } }.store(in: &cancellables) } + + private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() + + private func setUpSubscriptionMonitoring() { +#if SUBSCRIPTION + guard AccountManager().isUserAuthenticated else { return } + let entitlementsCheck = { + await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } + + Task { + await entitlementMonitor.start(entitlementCheck: entitlementsCheck) { [weak self] result in + switch result { + case .validEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = false + case .invalidEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = true + guard let self else { return } + Task { + let isConnected = await self.tunnelController.isConnected + if isConnected { + await self.tunnelController.stop() + DistributedNotificationCenter.default().post(.showExpiredEntitlementNotification) + } + } + case .error: + break + } + } + } +#endif + } } extension NSApplication { diff --git a/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift b/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift new file mode 100644 index 0000000000..b61f1779c1 --- /dev/null +++ b/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift @@ -0,0 +1,33 @@ +// +// NetworkProtection+VPNAgentConvenienceInitializers.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection + +#if SUBSCRIPTION +import Subscription +#endif + +extension NetworkProtectionKeychainTokenStore { + convenience init() { + self.init(keychainType: .default, + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false, + accessTokenProvider: { return nil }) + } +} diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 52963e7b61..e34540d016 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -22,6 +22,10 @@ import NetworkProtection import ServiceManagement import AppKit +#if SUBSCRIPTION +import Subscription +#endif + /// Class that implements the necessary logic to ensure the VPN is enabled, or prevent the app from running otherwise. /// final class NetworkProtectionBouncer { @@ -30,13 +34,31 @@ final class NetworkProtectionBouncer { /// current app. /// func requireAuthTokenOrKillApp(controller: TunnelController) async { +#if SUBSCRIPTION + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let result = await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + switch result { + case .success(true): + return + case .failure: + break + case .success(false): + os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized. Missing entitlement.") + await controller.stop() + + // EXIT_SUCCESS ensures the login item won't relaunch + // Ref: https://developer.apple.com/documentation/servicemanagement/smappservice/register() + // See where it mentions: + // "If the helper crashes or exits with a non-zero status, the system relaunches it" + exit(EXIT_SUCCESS) + } +#endif let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false, accessTokenProvider: { nil }) - guard keychainStore.isFeatureActivated else { - os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized.") + os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized. Missing token.") await controller.stop() diff --git a/DuckDuckGoVPN/VPNUninstaller.swift b/DuckDuckGoVPN/VPNUninstaller.swift new file mode 100644 index 0000000000..52593d9faa --- /dev/null +++ b/DuckDuckGoVPN/VPNUninstaller.swift @@ -0,0 +1,60 @@ +// +// VPNUninstaller.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection +import NetworkProtectionIPC + +protocol VPNUninstalling { + func uninstall(includingSystemExtension: Bool) async +} + +final class VPNUninstaller: VPNUninstalling { + let networkExtensionController: NetworkExtensionController + let vpnConfiguration: VPNConfigurationManager + let defaults: UserDefaults + + init(networkExtensionController: NetworkExtensionController, vpnConfigurationManager: VPNConfigurationManager, defaults: UserDefaults = .netP) { + self.networkExtensionController = networkExtensionController + self.vpnConfiguration = vpnConfigurationManager + self.defaults = defaults + } + + func uninstall(includingSystemExtension: Bool) async { +#if NETP_SYSTEM_EXTENSION + if includingSystemExtension { + do { + try await networkExtensionController.deactivateSystemExtension() + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) + } catch { + + } + } +#endif + + await vpnConfiguration.removeVPNConfiguration() + + if defaults.networkProtectionOnboardingStatus == .completed { + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) + } + + defaults.networkProtectionShouldShowVPNUninstalledMessage = true + + exit(EXIT_SUCCESS) + } +} diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index b1a72a5caa..0e2d71d1b3 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,6 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ .target( diff --git a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift index 87fda099f7..48a16a8689 100644 --- a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift +++ b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift @@ -52,6 +52,10 @@ public struct LoginItem: Equatable, Hashable { self == .enabled } + public var isInstalled: Bool { + self == .enabled || self == .requiresApproval + } + @available(macOS 13.0, *) public init(_ status: SMAppService.Status) { switch status { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift index 2e408557b0..e64f0a605a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift @@ -32,6 +32,7 @@ public enum AppLaunchCommand: Codable { case stopVPN case enableOnDemand case moveAppToApplications + case showPrivacyPro } public protocol AppLaunching { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift new file mode 100644 index 0000000000..8f85e818ea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift @@ -0,0 +1,44 @@ +// +// UserDefault+ShowVPNUninstalledMessage.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension UserDefaults { + private enum Key { + static var networkProtectionShouldShowVPNUninstalledMessage = "networkProtectionShouldShowVPNUninstalledMessage" + } + + // Convenience declaration + private var networkProtectionShowVPNUninstalledMessageRawValueKey: String { + Key.networkProtectionShouldShowVPNUninstalledMessage + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionShouldShowVPNUninstalledMessage: Bool { + get { + value(forKey: networkProtectionShowVPNUninstalledMessageRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionShowVPNUninstalledMessageRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift new file mode 100644 index 0000000000..397f67f6ac --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift @@ -0,0 +1,44 @@ +// +// UserDefault+VPNEnabledViaWaitlist.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension UserDefaults { + private enum Key { + static var networkProtectionVPNEnabledViaWaitlist = "networkProtectionVPNEnabledViaWaitlist" + } + + // Convenience declaration + private var networkProtectionVPNEnabledViaWaitlistRawValueKey: String { + Key.networkProtectionVPNEnabledViaWaitlist + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionVPNEnabledViaWaitlist: Bool { + get { + value(forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift new file mode 100644 index 0000000000..0768592eff --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift @@ -0,0 +1,44 @@ +// +// UserDefaults+NetworkProtectionExpiredEntitlements.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension UserDefaults { + private enum Key { + static var networkProtectionEntitlementsExpired = "networkProtectionEntitlementsExpired" + } + + // Convenience declaration + private var networkProtectionEntitlementsExpiredRawValueKey: String { + Key.networkProtectionEntitlementsExpired + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionEntitlementsExpired: Bool { + get { + value(forKey: networkProtectionEntitlementsExpiredRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionEntitlementsExpiredRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 1ec0e4d17a..402f85098f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -67,4 +67,11 @@ final class UserText { let localized = NSLocalizedString("network.protection.server.location.link", value: "%@...", comment: "Clickable text linking to the server location picker screen") return String(format: localized, location) } + + // MARK: Subscription Expired + + static let networkProtectionSubscriptionExpiredTitle = NSLocalizedString("network.protection.subscription.expired.title", value: "VPN disconnected", comment: "Title for the prompt that tells the user their subscription expired.") + static let networkProtectionSubscriptionExpiredSubtitle = NSLocalizedString("network.protection.subscription.expired.subtitle", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "Subtitle for the prompt that tells the user their subscription expired.") + static let networkProtectionSubscriptionExpiredResubscribeButton = NSLocalizedString("network.protection.subscription.expired.resubscribe.button", value: "Subscribe to Privacy Pro", comment: "Button for the prompt that takes the user to the page to resubscribe.") + static let networkProtectionSubscriptionExpiredUninstallButton = NSLocalizedString("network.protection.subscription.expired.uninstall.button", value: "Uninstall DuckDuckGo VPN", comment: "Button for the prompt that uninstalls the VPN.") } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 488970f7d4..1833f8482e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -56,7 +56,9 @@ public final class StatusBarMenu: NSObject { appLauncher: AppLaunching, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, - isMenuBarStatusView: Bool) { + isMenuBarStatusView: Bool, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.model = model let statusItem = statusItem ?? NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) @@ -69,7 +71,10 @@ public final class StatusBarMenu: NSObject { appLauncher: appLauncher, menuItems: menuItems, agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView) + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + uninstallHandler: uninstallHandler) + popover.behavior = .transient super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index efbe5872eb..0acbf84065 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -56,7 +56,9 @@ public final class NetworkProtectionPopover: NSPopover { appLauncher: AppLaunching, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, - isMenuBarStatusView: Bool) { + isMenuBarStatusView: Bool, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.statusReporter = statusReporter self.model = NetworkProtectionStatusView.Model(controller: controller, @@ -66,7 +68,9 @@ public final class NetworkProtectionPopover: NSPopover { appLauncher: appLauncher, menuItems: menuItems, agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView) + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + uninstallHandler: uninstallHandler) super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index a70e22f729..aae033b17d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -50,7 +50,14 @@ public struct NetworkProtectionStatusView: View { public var body: some View { VStack(spacing: 0) { - if let promptActionViewModel = model.promptActionViewModel { + if model.shouldShowSubscriptionExpired { + SubscriptionExpiredView { + model.openPrivacyPro() + } uninstallButtonHandler: { + model.uninstallVPN() + } + .padding(5) + } else if let promptActionViewModel = model.promptActionViewModel { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 454ec28102..7df51daf58 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -57,7 +57,7 @@ extension NetworkProtectionStatusView { private(set) var onboardingStatus: OnboardingStatus = .completed var tunnelControllerViewDisabled: Bool { - onboardingStatus != .completed || loginItemNeedsApproval + onboardingStatus != .completed || loginItemNeedsApproval || shouldShowSubscriptionExpired } @MainActor @@ -94,6 +94,10 @@ extension NetworkProtectionStatusView { /// private let runLoopMode: RunLoop.Mode? + private let appLauncher: AppLaunching + + private let uninstallHandler: () async -> Void + private var cancellables = Set() // MARK: - Dispatch Queues @@ -113,7 +117,9 @@ extension NetworkProtectionStatusView { menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, - runLoopMode: RunLoop.Mode? = nil) { + runLoopMode: RunLoop.Mode? = nil, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher @@ -123,6 +129,8 @@ extension NetworkProtectionStatusView { self.agentLoginItem = agentLoginItem self.isMenuBarStatusView = isMenuBarStatusView self.runLoopMode = runLoopMode + self.appLauncher = appLauncher + self.uninstallHandler = uninstallHandler tunnelControllerViewModel = TunnelControllerViewModel(controller: tunnelController, onboardingStatusPublisher: onboardingStatusPublisher, @@ -146,8 +154,14 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } + self?.onboardingStatus = status + } + .store(in: &cancellables) + + userDefaults + .publisher(for: \.networkProtectionEntitlementsExpired) + .receive(on: DispatchQueue.main) + .assign(to: \.shouldShowSubscriptionExpired, onWeaklyHeld: self) .store(in: &cancellables) } @@ -164,6 +178,18 @@ extension NetworkProtectionStatusView { } } + func openPrivacyPro() { + Task { + await appLauncher.launchApp(withCommand: .showPrivacyPro) + } + } + + func uninstallVPN() { + Task { + await uninstallHandler() + } + } + private func subscribeToStatusChanges() { statusReporter.statusObserver.publisher .receive(on: DispatchQueue.main) @@ -280,6 +306,9 @@ extension NetworkProtectionStatusView { let tunnelControllerViewModel: TunnelControllerViewModel + @Published + var shouldShowSubscriptionExpired: Bool = false + var promptActionViewModel: PromptActionView.Model? { #if !APPSTORE && !DEBUG guard Bundle.main.isInApplicationDirectory else { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift new file mode 100644 index 0000000000..a4fe590b8e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift @@ -0,0 +1,74 @@ +// +// SubscriptionExpiredView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import SwiftUIExtensions + +struct SubscriptionExpiredView: View { + enum Constants { + static let backgroundCornerRadius = 6.0 + } + + let subscribeButtonHandler: () -> Void + let uninstallButtonHandler: () -> Void + + public var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text(UserText.networkProtectionSubscriptionExpiredTitle) + .font(.system(size: 13).weight(.bold)) + .foregroundColor(Color(.defaultText)) + .multilineText() + + Text(UserText.networkProtectionSubscriptionExpiredSubtitle) + .font(.system(size: 13)) + .foregroundColor(Color(.defaultText)) + .multilineText() + + Button(UserText.networkProtectionSubscriptionExpiredResubscribeButton, action: subscribeButtonHandler) + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + .padding(.top, 3) + + Divider() + .padding(.top, 8) + .padding(.bottom, 3) + + Button(UserText.networkProtectionSubscriptionExpiredUninstallButton, action: uninstallButtonHandler) + .buttonStyle(.borderless) + .foregroundColor(.accentColor) + .padding(.top, 3) + } + .padding(.vertical, 16) + .padding(.horizontal, 10) + .cornerRadius(8) + .background( + RoundedRectangle(cornerRadius: Constants.backgroundCornerRadius, style: .circular) + .stroke(Color(.onboardingStepBorder), lineWidth: 1) + .background( + RoundedRectangle(cornerRadius: Constants.backgroundCornerRadius, style: .circular) + .fill(Color(.onboardingStepBackground)) + ) + ) + } +} + +struct SubscriptionExpiredView_Preview: PreviewProvider { + static var previews: some View { + SubscriptionExpiredView(subscribeButtonHandler: {}, uninstallButtonHandler: {}) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index b680b062b6..5b9e7e0dea 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -56,7 +56,9 @@ final class StatusBarMenuTests: XCTestCase { appLauncher: MockAppLauncher(), menuItems: { [] }, agentLoginItem: nil, - isMenuBarStatusView: false) + isMenuBarStatusView: false, + userDefaults: .standard, + uninstallHandler: { }) menu.show() @@ -79,7 +81,9 @@ final class StatusBarMenuTests: XCTestCase { appLauncher: MockAppLauncher(), menuItems: { [] }, agentLoginItem: nil, - isMenuBarStatusView: false) + isMenuBarStatusView: false, + userDefaults: .standard, + uninstallHandler: { }) menu.hide() diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 63994d4a45..4693eb6b4a 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,6 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index bf7cce608f..58eeb26cc1 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift index 7787d3d93b..8f2ffe08d6 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift +++ b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift @@ -48,11 +48,11 @@ final class NetworkProtectionAgentNotificationsPresenter: NetworkProtectionNotif notificationCenter.post(.showVPNSupersededNotification) } - func showTestNotification() { - notificationCenter.post(.showTestNotification) + func showEntitlementNotification() { + notificationCenter.post(.showExpiredEntitlementNotification) } - func showEntitlementNotification() { - // todo + func showTestNotification() { + notificationCenter.post(.showTestNotification) } } From 604e6be6151348d731f7d27485337e77d597e6fb Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 19 Mar 2024 13:00:27 -0700 Subject: [PATCH 15/17] Add Privacy Pro to App Store build (#2440) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206861044153287/f Tech Design URL: CC: Description: This PR adds Privacy Pro to the App Store build. --- Configuration/Common.xcconfig | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 27 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 4 +-- ...kDuckGo Privacy Browser App Store.xcscheme | 3 +++ .../DuckDuckGoVPNAppStore.entitlements | 1 + .../InputFilesChecker/InputFilesChecker.swift | 2 -- LocalPackages/LoginItems/Package.swift | 2 +- .../XCTestCase+PixelKit.swift | 3 +++ LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- 10 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index 5c38d4e8e4..392d63d532 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -21,7 +21,7 @@ COMBINE_HIDPI_IMAGES = YES DEVELOPMENT_TEAM = HKE973VLUW DEVELOPMENT_TEAM[config=CI][sdk=*] = -FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION +FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION SUBSCRIPTION GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1596da5917..a80901b936 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2111,6 +2111,11 @@ 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF09222830812900EE1418 /* FileSystemDSL.swift */; }; 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0924283083EC00EE1418 /* FileSystemDSLTests.swift */; }; 4BC2621D293996410087A482 /* PixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC2621C293996410087A482 /* PixelEventTests.swift */; }; + 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2F565B2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift */; }; + 4BCBE4562BA7E16900FC75A1 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */; }; + 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */; }; + 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4592BA7E17800FC75A1 /* Subscription */; }; + 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE45B2BA7E18500FC75A1 /* Subscription */; }; 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */; }; @@ -4683,10 +4688,12 @@ 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */, 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */, + 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */, 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */, 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, 4BF97AD12B43C43F00EB4240 /* NetworkProtectionIPC in Frameworks */, 37F44A5F298C17830025E7FE /* Navigation in Frameworks */, + 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, @@ -4772,6 +4779,7 @@ buildActionMask = 2147483647; files = ( 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, + 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, @@ -8691,7 +8699,9 @@ 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, 85E2BBCF2B8F534A00DBEC7A /* History */, F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */, + 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */, 85D44B872BA08D30001B4AB5 /* Suggestions */, + 4BCBE4592BA7E17800FC75A1 /* Subscription */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8877,6 +8887,7 @@ 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, + 4BCBE45B2BA7E18500FC75A1 /* Subscription */, ); productName = DuckDuckGoAgentAppStore; productReference = 4B2D06692A13318400DE1F49 /* DuckDuckGo VPN App Store.app */; @@ -10611,6 +10622,7 @@ 3706FC43293F65D500E42796 /* PinnedTabsViewModel.swift in Sources */, 85D0327C2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, B6685E4329A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, + 4BCBE4562BA7E16900FC75A1 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, 3706FC44293F65D500E42796 /* BookmarkList.swift in Sources */, 3706FC45293F65D500E42796 /* BookmarkTableRowView.swift in Sources */, 7BEC20462B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift in Sources */, @@ -10693,6 +10705,7 @@ 3706FC81293F65D500E42796 /* DispatchQueueExtensions.swift in Sources */, C13909F02B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, + 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -14492,6 +14505,20 @@ isa = XCSwiftPackageProductDependency; productName = LoginItems; }; + 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */ = { + isa = XCSwiftPackageProductDependency; + productName = SubscriptionUI; + }; + 4BCBE4592BA7E17800FC75A1 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + 4BCBE45B2BA7E18500FC75A1 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; 4BF97AD02B43C43F00EB4240 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7d241ed988..462aa74011 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "graeme/expired-entitlements-stuff", - "revision" : "925d0dd50e47f38c7fe922622002e8961569bc32" + "branch" : "126.2.0", + "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme index 29eecc0cc8..f5feca4eef 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme @@ -106,6 +106,9 @@ + + diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements index 5e706138b5..234e25f597 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements @@ -4,6 +4,7 @@ com.apple.developer.networking.networkextension + app-proxy-provider packet-tunnel-provider com.apple.security.app-sandbox diff --git a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index 00053f594d..cd6186444d 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -26,8 +26,6 @@ let nonSandboxedExtraInputFiles: Set = [ .init("BWManager.swift", .source), .init("UpdateController.swift", .source), .init("PFMoveApplication.m", .source), - .init("NetworkProtectionSubscriptionEventHandler.swift", .source), - .init("DataBrokerProtectionSubscriptionEventHandler.swift", .source), .init("DuckDuckGo VPN.app", .unknown), .init("DuckDuckGo Notifications.app", .unknown), .init("DuckDuckGo Personal Information Removal.app", .unknown) diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index 0e2d71d1b3..389c3f9d97 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift index 6d742bd532..9017d8774a 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -111,6 +111,9 @@ public extension XCTestCase { let knownExpectedParameters = knownExpectedParameters(for: event) let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + // Ensure PixelKit is torn down before setting it back up, avoiding unit test race conditions: + PixelKit.tearDown() + PixelKit.setUp(dryRun: false, appVersion: "1.0.5", source: "test-app", diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 4693eb6b4a..b01805eb86 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 58eeb26cc1..53f7b3e21b 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. From 59a72eaac95f500c4b04ecc3a624c969db36cdb2 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 19 Mar 2024 21:13:52 +0100 Subject: [PATCH 16/17] Use SubscriptionFeatureAvailability to determine availability of the subscription (#2436) Task/Issue URL: https://app.asana.com/0/0/1206841322374473/f **Description**: Use BSK provided SubscriptionFeatureAvailability to show or hide Subscription experience in the UI. **Steps to test this PR**: Test internal user override 1. Enable internal user 2. More options menu and settings should display Privacy Pro Test privacy config with flags disabled 1. Disable internal user 2. Prepare and host modified privacy config disabling all privacyPro feature flags: - `isLaunched` - `isLaunchedStripe` - `allowPurchase` - `allowPurchaseStripe` - `isLaunchedOverride` - `isLaunchedOverrideStripe` 4. Privacy Pro should not be accessible in more options menu and in settings Test build with purchase via `App Store` enabling only: - `isLaunched` and `allowPurchase` - `isLaunchedOverride` and `allowPurchase` Result: Privacy Pro options should be available - `isLaunchedStripe` and `allowPurchaseStripe` - `isLaunchedOverrideStripe` and `allowPurchaseStripe` Result: Privacy Pro options should be disabled Test build with purchase via `Stripe` enabling only: - `isLaunchedStripe` and `allowPurchaseStripe` - `isLaunchedOverrideStripe` and `allowPurchaseStripe` Result: Privacy Pro options should be available - `isLaunched` and `allowPurchase` - `isLaunchedOverride` and `allowPurchase` Result: Privacy Pro options should be disabled Test build enabling all flags except `allowPurchase` and `allowPurchaseStripe`: Result: - Privacy Pro options should be available - Opening purchase page should not show the purchase options disallowing to purchase --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Fernando Bunn --- DuckDuckGo.xcodeproj/project.pbxproj | 13 +--- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/Application/AppDelegate.swift | 19 +++-- .../Utilities/UserDefaultsWrapper.swift | 1 + .../DBP/DataBrokerProtectionAppEvents.swift | 8 +- ...ataBrokerProtectionFeatureVisibility.swift | 31 +++++++- .../View/AddressBarTextField.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 11 +-- .../View/NavigationBarViewController.swift | 2 +- ...rkProtection+ConvenienceInitializers.swift | 9 ++- .../Model/PreferencesSection.swift | 2 +- .../SubscriptionFeatureAvailability.swift | 76 ------------------- .../SubscriptionPagesUserScript.swift | 2 + DuckDuckGo/Tab/UserScripts/UserScripts.swift | 2 +- .../WaitlistViewControllerPresenter.swift | 5 ++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- 20 files changed, 80 insertions(+), 117 deletions(-) delete mode 100644 DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a80901b936..6329646836 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -165,9 +165,6 @@ 1E950E432912A10D0051A99B /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E422912A10D0051A99B /* UserScript */; }; 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */; }; 1EA7B8D52B7E078C000330A4 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1EA7B8D42B7E078C000330A4 /* Subscription */; }; - 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */; }; - 1EA7B8D92B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */; }; - 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */; }; 1ED910D52B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 1ED910D72B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; @@ -3525,7 +3522,6 @@ 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockingRulesUpdateObserver.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; - 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeatureAvailability.swift; sourceTree = ""; }; 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -5135,7 +5131,6 @@ isa = PBXGroup; children = ( EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */, - 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */, ); path = Subscription; sourceTree = ""; @@ -10124,7 +10119,6 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, @@ -10404,7 +10398,6 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 1EA7B8D92B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, @@ -11718,7 +11711,6 @@ 4B957AF22AC7AE700062CA31 /* DailyPixel.swift in Sources */, 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 4B957AF32AC7AE700062CA31 /* NavigationHotkeyHandler.swift in Sources */, - 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4B957AF42AC7AE700062CA31 /* ClickToLoadUserScript.swift in Sources */, 4B957AF52AC7AE700062CA31 /* WindowControllersManager.swift in Sources */, 4B957AF62AC7AE700062CA31 /* FireAnimationView.swift in Sources */, @@ -11757,9 +11749,8 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */, - 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, + 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, 4B957B1B2AC7AE700062CA31 /* ScriptSourceProviding.swift in Sources */, @@ -12085,7 +12076,6 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 1D220BF82B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, @@ -12782,7 +12772,6 @@ 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, - 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, 4BE53374286E39F10019DBFD /* ChromiumKeychainPrompt.swift in Sources */, B6553692268440D700085A79 /* WKProcessPool+GeolocationProvider.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 462aa74011..91ab528b64 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "126.2.0", - "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9" + "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9", + "version" : "126.2.0" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index e4f938425a..0be2485308 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -81,6 +81,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? +#if SUBSCRIPTION + let subscriptionFeatureAvailability: SubscriptionFeatureAvailability +#endif + #if NETWORK_PROTECTION && SUBSCRIPTION // Needs to be lazy as indirectly depends on AppDelegate private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() @@ -183,6 +187,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfig: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig) +#if SUBSCRIPTION + #if APPSTORE || !STRIPE + SubscriptionPurchaseEnvironment.current = .appStore + #else + SubscriptionPurchaseEnvironment.current = .stripe + #endif + subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, + purchasePlatform: SubscriptionPurchaseEnvironment.current) +#endif } func applicationWillFinishLaunching(_ notification: Notification) { @@ -247,12 +260,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel defaultValue: defaultEnvironment).wrappedValue SubscriptionPurchaseEnvironment.currentServiceEnvironment = currentEnvironment - #if APPSTORE || !STRIPE - SubscriptionPurchaseEnvironment.current = .appStore - #else - SubscriptionPurchaseEnvironment.current = .stripe - #endif - Task { let accountManager = AccountManager() do { diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index da443d381e..e11769ae4e 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -147,6 +147,7 @@ public struct UserDefaultsWrapper { case dataBrokerProtectionTermsAndConditionsAccepted = "data-broker-protection.waitlist-terms-and-conditions.accepted" case shouldShowDBPWaitlistInvitedCardUI = "shouldShowDBPWaitlistInvitedCardUI" + case dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro = "data-broker-protection.cleaned-up-from-waitlist-to-privacy-pro" // VPN diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 11292e7ac3..d16a781a99 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -35,7 +35,9 @@ struct DataBrokerProtectionAppEvents { let loginItemsManager = LoginItemsManager() let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - guard featureVisibility.isFeatureVisible() else { + guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + + guard featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() else { featureVisibility.disableAndDeleteForWaitlistUsers() return } @@ -58,7 +60,9 @@ struct DataBrokerProtectionAppEvents { func applicationDidBecomeActive() { let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - guard featureVisibility.isFeatureVisible() else { + guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + + guard featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() else { featureVisibility.disableAndDeleteForWaitlistUsers() return } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift index 9065dfcaf0..3f7f04f97f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift @@ -27,22 +27,29 @@ protocol DataBrokerProtectionFeatureVisibility { func isFeatureVisible() -> Bool func disableAndDeleteForAllUsers() func disableAndDeleteForWaitlistUsers() + func isPrivacyProEnabled() -> Bool } struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeatureVisibility { private let privacyConfigurationManager: PrivacyConfigurationManaging private let featureDisabler: DataBrokerProtectionFeatureDisabling private let pixelHandler: EventMapping + private let userDefaults: UserDefaults + + @UserDefaultsWrapper(key: .dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro, defaultValue: false) + var dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro: Bool /// Temporary code to use while we have both redeem flow for diary study users. Should be removed later static var bypassWaitlist = false init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler(), - pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler()) { + pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), + userDefaults: UserDefaults = .standard) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler + self.userDefaults = userDefaults } var waitlistIsOngoing: Bool { @@ -84,6 +91,15 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature DataBrokerProtectionWaitlist().waitlistStorage.isWaitlistUser } + func isPrivacyProEnabled() -> Bool { +#if SUBSCRIPTION + return NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable +#else + return false +#endif + + } + func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() @@ -99,6 +115,19 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature featureDisabler.disableAndDelete() } + /// Returns true if a cleanup was performed, false otherwise + func cleanUpDBPForPrivacyProIfNecessary() -> Bool { + let wasWaitlistUser = DataBrokerProtectionWaitlist().waitlistStorage.getWaitlistInviteCode() != nil + + if isPrivacyProEnabled() && wasWaitlistUser && !dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro { + disableAndDeleteForWaitlistUsers() + dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro = true + return true + } else { + return false + } + } + /// If we want to prevent new users from joining the waitlist while still allowing waitlist users to continue using it, /// we should set isWaitlistEnabled to false and isWaitlistBetaActive to true. /// To remove it from everyone, isWaitlistBetaActive should be set to false diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 75bcc66066..bad6b6ab63 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -347,7 +347,7 @@ final class AddressBarTextField: NSTextField { #endif #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { if providedUrl.isChild(of: URL.subscriptionBaseURL) || providedUrl.isChild(of: URL.identityTheftRestoration) { self.updateValue(selectedTabViewModel: nil, addressBarString: nil) // reset self.window?.makeFirstResponder(nil) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index b2dd2d0a7f..26fc57d85b 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -318,7 +318,7 @@ final class MoreOptionsMenu: NSMenu { var items: [NSMenuItem] = [] #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && !AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && !AccountManager().isUserAuthenticated { items.append(contentsOf: makeInactiveSubscriptionItems()) } else { items.append(contentsOf: makeActiveSubscriptionItems()) // this adds NETP and DBP only if conditionally enabled @@ -345,7 +345,7 @@ final class MoreOptionsMenu: NSMenu { items.append(networkProtectionItem) #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && AccountManager().isUserAuthenticated { Task { let isMenuItemEnabled: Bool @@ -368,7 +368,8 @@ final class MoreOptionsMenu: NSMenu { #endif // NETWORK_PROTECTION #if DBP - if DefaultDataBrokerProtectionFeatureVisibility().isFeatureVisible() { + let dbpVisibility = DefaultDataBrokerProtectionFeatureVisibility() + if dbpVisibility.isFeatureVisible() || dbpVisibility.isPrivacyProEnabled() { let dataBrokerProtectionItem = NSMenuItem(title: UserText.dataBrokerProtectionOptionsMenuItem, action: #selector(openDataBrokerProtection), keyEquivalent: "") @@ -377,7 +378,7 @@ final class MoreOptionsMenu: NSMenu { items.append(dataBrokerProtectionItem) #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && AccountManager().isUserAuthenticated { Task { let isMenuItemEnabled: Bool @@ -409,7 +410,7 @@ final class MoreOptionsMenu: NSMenu { .withImage(.itrIcon) items.append(identityTheftRestorationItem) - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && AccountManager().isUserAuthenticated { Task { let isMenuItemEnabled: Bool diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index fbe69492b5..bfb86ac0e1 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -327,7 +327,7 @@ final class NavigationBarViewController: NSViewController { } #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { let accountManager = AccountManager() let networkProtectionTokenStorage = NetworkProtectionKeychainTokenStore() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 8eed556fac..9a961818cc 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -28,6 +28,7 @@ import Subscription extension NetworkProtectionDeviceManager { + @MainActor static func create() -> NetworkProtectionDeviceManager { let settings = VPNSettings(defaults: .netP) let keyStore = NetworkProtectionKeychainKeyStore() @@ -36,7 +37,7 @@ extension NetworkProtectionDeviceManager { tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable) } } @@ -46,7 +47,7 @@ extension NetworkProtectionCodeRedemptionCoordinator { self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable) } } @@ -59,7 +60,7 @@ extension NetworkProtectionKeychainTokenStore { #endif self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable(), + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable, accessTokenProvider: accessTokenProvider) } } @@ -78,7 +79,7 @@ extension NetworkProtectionLocationListCompositeRepository { environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable() + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable ) } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 785350403e..b05bcc4dd9 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -60,7 +60,7 @@ struct PreferencesSection: Hashable, Identifiable { ] #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { let subscriptionPanes: [PreferencePaneIdentifier] = [.subscription] sections.insert(.init(id: .privacyPro, panes: subscriptionPanes), at: 1) } diff --git a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift deleted file mode 100644 index 9090ffad02..0000000000 --- a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// SubscriptionFeatureAvailability.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit - -#if SUBSCRIPTION -import Subscription -#endif - -#if NETWORK_PROTECTION -import NetworkProtection -import BrowserServicesKit -#endif - -protocol SubscriptionFeatureAvailability { - func isFeatureAvailable() -> Bool -} - -struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { - - func isFeatureAvailable() -> Bool { -#if SUBSCRIPTION_OVERRIDE_ENABLED - return true -#elseif SUBSCRIPTION - print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isDBPActivated: [\(isDBPActivated)]") - return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isDBPActivated) -#else - return false -#endif - } - - private var isSubscriptionInternalTestingEnabled: Bool { - UserDefaultsWrapper(key: .subscriptionInternalTesting, defaultValue: false).wrappedValue - } - - private var isInternalUser: Bool { - Self.internalUserDecider.isInternalUser - } - - private var isDBPActivated: Bool { -#if DBP - return DataBrokerProtectionManager.shared.dataManager.fetchProfile(ignoresCache: true) != nil -#else - return false -#endif - } - - private static var internalUserDecider: InternalUserDecider = { - let keyStore = EncryptionKeyStore() - let fileStore: FileStore - do { - let encryptionKey = NSApplication.runType.requiresEnvironment ? try keyStore.readKey() : nil - fileStore = EncryptedFileStore(encryptionKey: encryptionKey) - } catch { - fileStore = EncryptedFileStore() - } - - let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) - return DefaultInternalUserDecider(store: internalUserDeciderStore) - }() -} diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 3c08e86e73..f270783d8d 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -185,6 +185,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard await NSApp.delegateTyped.subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed else { return SubscriptionOptions.empty } + if SubscriptionPurchaseEnvironment.current == .appStore { if #available(macOS 12.0, *) { switch await AppStorePurchaseFlow.subscriptionOptions() { diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 4dfa1c5b50..d4cb62cdce 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -87,7 +87,7 @@ final class UserScripts: UserScriptsProvider { } #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature()) userScripts.append(subscriptionPagesUserScript) diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index fba944d198..f51b0b695c 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -81,6 +81,11 @@ struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerP struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { static func shouldPresentWaitlist() -> Bool { +#if SUBSCRIPTION + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { + return false + } +#endif let waitlist = DataBrokerProtectionWaitlist() let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.dataBrokerProtectionTermsAndConditionsAccepted.rawValue) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 51a9a0276c..25aa35182d 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 85d865a47b..2dc7d68c5c 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 90909e471e..f2568a8053 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index b01805eb86..1728c280de 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 53f7b3e21b..b088e62b79 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. From 04c2026176488b70cb83b26dddfac9474b46fe9e Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 19 Mar 2024 22:05:16 +0000 Subject: [PATCH 17/17] Connect thank you message to DBP method (#2456) Task/Issue URL: https://app.asana.com/0/1204167627774280/1206824862073996/f Tech Design URL: CC: **Description**: Connect thank you message to DBP method --- .../DBP/DataBrokerProtectionFeatureVisibility.swift | 11 +++++++++-- .../Views/WaitlistThankYouPromptPresenter.swift | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift index 3f7f04f97f..46de122bd1 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift @@ -28,6 +28,7 @@ protocol DataBrokerProtectionFeatureVisibility { func disableAndDeleteForAllUsers() func disableAndDeleteForWaitlistUsers() func isPrivacyProEnabled() -> Bool + func isEligibleForThankYouMessage() -> Bool } struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeatureVisibility { @@ -91,6 +92,10 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature DataBrokerProtectionWaitlist().waitlistStorage.isWaitlistUser } + private var wasWaitlistUser: Bool { + DataBrokerProtectionWaitlist().waitlistStorage.getWaitlistInviteCode() != nil + } + func isPrivacyProEnabled() -> Bool { #if SUBSCRIPTION return NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable @@ -100,6 +105,10 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature } + func isEligibleForThankYouMessage() -> Bool { + return wasWaitlistUser && isPrivacyProEnabled() + } + func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() @@ -117,8 +126,6 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature /// Returns true if a cleanup was performed, false otherwise func cleanUpDBPForPrivacyProIfNecessary() -> Bool { - let wasWaitlistUser = DataBrokerProtectionWaitlist().waitlistStorage.getWaitlistInviteCode() != nil - if isPrivacyProEnabled() && wasWaitlistUser && !dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro { disableAndDeleteForWaitlistUsers() dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro = true diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 81fa1f9179..78eecff705 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -34,7 +34,7 @@ final class WaitlistThankYouPromptPresenter { self.init(isVPNBetaTester: { return false }, isPIRBetaTester: { - return false + return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() }) }