diff --git a/.maestro/ad_click_detection_flows/10_-_m.js_bing-provided;_ad_domain_provided_but_incorrect_(dsl_not_needed).yaml b/.maestro/ad_click_detection_flows/10_-_m.js_bing-provided;_ad_domain_provided_but_incorrect_(dsl_not_needed).yaml index 0ceea023b48f..8aec68aa04ea 100644 --- a/.maestro/ad_click_detection_flows/10_-_m.js_bing-provided;_ad_domain_provided_but_incorrect_(dsl_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/10_-_m.js_bing-provided;_ad_domain_provided_but_incorrect_(dsl_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-10" - tapOn: diff --git a/.maestro/ad_click_detection_flows/11_-_y.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)_(u3_not_needed).yaml b/.maestro/ad_click_detection_flows/11_-_y.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)_(u3_not_needed).yaml index 7d8872dd4c4a..3ea65b724986 100644 --- a/.maestro/ad_click_detection_flows/11_-_y.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)_(u3_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/11_-_y.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)_(u3_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-11" - tapOn: diff --git a/.maestro/ad_click_detection_flows/12_-_m.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)__(dsl_not_needed).yaml b/.maestro/ad_click_detection_flows/12_-_m.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)__(dsl_not_needed).yaml index 72e9df36e591..60518d18cb23 100644 --- a/.maestro/ad_click_detection_flows/12_-_m.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)__(dsl_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/12_-_m.js_bing-provided;_ad_domain_provided_but_it's_not_a_domain_(i.e.,_abcedf)__(dsl_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-12" - tapOn: diff --git a/.maestro/ad_click_detection_flows/13_-_y.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)_(u3_not_needed).yaml b/.maestro/ad_click_detection_flows/13_-_y.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)_(u3_not_needed).yaml index 4368591e517f..71a9bb7f7f89 100644 --- a/.maestro/ad_click_detection_flows/13_-_y.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)_(u3_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/13_-_y.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)_(u3_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-13" - tapOn: diff --git a/.maestro/ad_click_detection_flows/14_-_m.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)__(dsl_not_needed).yaml b/.maestro/ad_click_detection_flows/14_-_m.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)__(dsl_not_needed).yaml index 367acf429c5a..c56e646d6dfa 100644 --- a/.maestro/ad_click_detection_flows/14_-_m.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)__(dsl_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/14_-_m.js_bing-provided;_ad_domain_provided_but_it's_a_subdomain_of_advertiser_(i.e.,_foo.www.search-company-site)__(dsl_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-14" - tapOn: diff --git a/.maestro/ad_click_detection_flows/1_-_y.js_heuristic;_no_ad_domain_param;_u3_param_included.yaml b/.maestro/ad_click_detection_flows/1_-_y.js_heuristic;_no_ad_domain_param;_u3_param_included.yaml index a3dec88cb267..0ef176c05d60 100644 --- a/.maestro/ad_click_detection_flows/1_-_y.js_heuristic;_no_ad_domain_param;_u3_param_included.yaml +++ b/.maestro/ad_click_detection_flows/1_-_y.js_heuristic;_no_ad_domain_param;_u3_param_included.yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-1" - tapOn: @@ -38,7 +38,7 @@ tags: - assertVisible: text: "convert.ad-company.site" - assertVisible: - text: "ad-company.site" + text: "ad-company.site" - action: back - assertVisible: text: "View Non-Tracker Companies" diff --git a/.maestro/ad_click_detection_flows/2_-_m.js_heuristic;_no_ad_domain_param;_dsl_param_included.yaml b/.maestro/ad_click_detection_flows/2_-_m.js_heuristic;_no_ad_domain_param;_dsl_param_included.yaml index fdeaedc8ace6..72a9657fc690 100644 --- a/.maestro/ad_click_detection_flows/2_-_m.js_heuristic;_no_ad_domain_param;_dsl_param_included.yaml +++ b/.maestro/ad_click_detection_flows/2_-_m.js_heuristic;_no_ad_domain_param;_dsl_param_included.yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-2" - tapOn: diff --git a/.maestro/ad_click_detection_flows/3_-_y.js_heuristic;_no_ad_domain_param,_but_missing_u3_param.yaml b/.maestro/ad_click_detection_flows/3_-_y.js_heuristic;_no_ad_domain_param,_but_missing_u3_param.yaml index 6f03754c53c9..8602adaf986a 100644 --- a/.maestro/ad_click_detection_flows/3_-_y.js_heuristic;_no_ad_domain_param,_but_missing_u3_param.yaml +++ b/.maestro/ad_click_detection_flows/3_-_y.js_heuristic;_no_ad_domain_param,_but_missing_u3_param.yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-3" - tapOn: diff --git a/.maestro/ad_click_detection_flows/4_-_m.js_heuristic;_no_ad_domain_param,_but_missing_dsl_param.yaml b/.maestro/ad_click_detection_flows/4_-_m.js_heuristic;_no_ad_domain_param,_but_missing_dsl_param.yaml index 4d4150810b94..d01859f75c24 100644 --- a/.maestro/ad_click_detection_flows/4_-_m.js_heuristic;_no_ad_domain_param,_but_missing_dsl_param.yaml +++ b/.maestro/ad_click_detection_flows/4_-_m.js_heuristic;_no_ad_domain_param,_but_missing_dsl_param.yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-4" - tapOn: diff --git a/.maestro/ad_click_detection_flows/5_-_y.js_heuristic;_ad_domain_provided,_but_empty_(u3_not_needed).yaml b/.maestro/ad_click_detection_flows/5_-_y.js_heuristic;_ad_domain_provided,_but_empty_(u3_not_needed).yaml index 39763d3a7d79..77bfcdeb067f 100644 --- a/.maestro/ad_click_detection_flows/5_-_y.js_heuristic;_ad_domain_provided,_but_empty_(u3_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/5_-_y.js_heuristic;_ad_domain_provided,_but_empty_(u3_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/ad_click_detection_flows/6_-_m.js_heuristic;_ad_domain_provided,_but_empty_(dsl_not_needed).yaml b/.maestro/ad_click_detection_flows/6_-_m.js_heuristic;_ad_domain_provided,_but_empty_(dsl_not_needed).yaml index ac44ce94465d..497894c02606 100644 --- a/.maestro/ad_click_detection_flows/6_-_m.js_heuristic;_ad_domain_provided,_but_empty_(dsl_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/6_-_m.js_heuristic;_ad_domain_provided,_but_empty_(dsl_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -11,12 +11,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-6" - tapOn: diff --git a/.maestro/ad_click_detection_flows/7_-_y.js_bing-provided;_ad_domain_provided_(u3_not_needed).yaml b/.maestro/ad_click_detection_flows/7_-_y.js_bing-provided;_ad_domain_provided_(u3_not_needed).yaml index ecedc4749564..03c5726d820f 100644 --- a/.maestro/ad_click_detection_flows/7_-_y.js_bing-provided;_ad_domain_provided_(u3_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/7_-_y.js_bing-provided;_ad_domain_provided_(u3_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-7" - tapOn: diff --git a/.maestro/ad_click_detection_flows/8_-_m.js_bing-provided;_ad_domain_provided_(dsl_not_needed).yaml b/.maestro/ad_click_detection_flows/8_-_m.js_bing-provided;_ad_domain_provided_(dsl_not_needed).yaml index 63f0f7db37dd..1eed52397431 100644 --- a/.maestro/ad_click_detection_flows/8_-_m.js_bing-provided;_ad_domain_provided_(dsl_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/8_-_m.js_bing-provided;_ad_domain_provided_(dsl_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-8" - tapOn: diff --git a/.maestro/ad_click_detection_flows/9_-_y.js_bing-provided;_ad_domain_provided_but_incorrect_(u3_not_needed).yaml b/.maestro/ad_click_detection_flows/9_-_y.js_bing-provided;_ad_domain_provided_but_incorrect_(u3_not_needed).yaml index 4b6346a8c1c5..89f7bd91f795 100644 --- a/.maestro/ad_click_detection_flows/9_-_y.js_bing-provided;_ad_domain_provided_but_incorrect_(u3_not_needed).yaml +++ b/.maestro/ad_click_detection_flows/9_-_y.js_bing-provided;_ad_domain_provided_but_incorrect_(u3_not_needed).yaml @@ -1,6 +1,6 @@ appId: com.duckduckgo.mobile.android tags: - - adClickTest + - adClickTest --- - launchApp: clearState: true @@ -12,11 +12,11 @@ tags: - assertVisible: text: ".*Got It.*" - tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" + text: "Got It" +- tapOn: + id: "com.duckduckgo.mobile.android:id/fireIconMenu" - tapOn: - text: "HIDE TIPS FOREVER" + text: "Cancel" - assertVisible: id: "ad-id-9" - tapOn: diff --git a/.maestro/ads_preview_flows/1-_design-system-components.yaml b/.maestro/ads_preview_flows/1-_design-system-components.yaml index b5555f774300..af6f0181bb10 100644 --- a/.maestro/ads_preview_flows/1-_design-system-components.yaml +++ b/.maestro/ads_preview_flows/1-_design-system-components.yaml @@ -66,7 +66,7 @@ tags: - tapOn: id: "com.duckduckgo.mobile.android:id/primaryCta" - tapOn: "LAYOUTS" -- tapOn: "Primary" +- assertVisible: "Expandable Layout" - tapOn: "INTERACTIVE" - tapOn: id: "com.duckduckgo.mobile.android:id/switch_one" diff --git a/.maestro/custom_tabs/custom_tabs_navigation.yaml b/.maestro/custom_tabs/custom_tabs_navigation.yaml index 3598fe4b8778..b17a0c86cca7 100644 --- a/.maestro/custom_tabs/custom_tabs_navigation.yaml +++ b/.maestro/custom_tabs/custom_tabs_navigation.yaml @@ -8,8 +8,11 @@ tags: stopApp: true - assertVisible: - text: ".*Not to worry! Searching and browsing privately.*" + text: ".*Ready for a better, more private internet?.*" - tapOn: "let's do it!" +- assertVisible: + text: ".*Privacy protections activated.*" +- tapOn: "choose your browser" - runFlow: when: visible: "set as default" @@ -17,7 +20,8 @@ tags: - tapOn: "duckduckgo" - tapOn: "set as default" - assertVisible: - text: ".*I'll also upgrade the security of your connection if possible.*" + text: ".*Awesome! Welcome to the duck side.*" + - tapOn: "start browsing" - tapOn: id: "com.duckduckgo.mobile.android:id/browserMenuImageView" diff --git a/.maestro/custom_tabs/custom_tabs_navigation_new_tab.yaml b/.maestro/custom_tabs/custom_tabs_navigation_new_tab.yaml index f4c4ba5674a6..84bb91b15edf 100644 --- a/.maestro/custom_tabs/custom_tabs_navigation_new_tab.yaml +++ b/.maestro/custom_tabs/custom_tabs_navigation_new_tab.yaml @@ -8,8 +8,11 @@ tags: stopApp: true - assertVisible: - text: ".*Not to worry! Searching and browsing privately.*" + text: ".*Ready for a better, more private internet?.*" - tapOn: "let's do it!" +- assertVisible: + text: ".*Privacy protections activated.*" +- tapOn: "choose your browser" - runFlow: when: visible: "set as default" @@ -17,7 +20,8 @@ tags: - tapOn: "duckduckgo" - tapOn: "set as default" - assertVisible: - text: ".*I'll also upgrade the security of your connection if possible.*" + text: ".*Awesome! Welcome to the duck side.*" + - tapOn: "start browsing" - tapOn: id: "com.duckduckgo.mobile.android:id/browserMenuImageView" diff --git a/.maestro/fire_button/fire_during_onboarding.yaml b/.maestro/fire_button/fire_during_onboarding.yaml index 3d355d3dd777..ea5da487ba66 100644 --- a/.maestro/fire_button/fire_during_onboarding.yaml +++ b/.maestro/fire_button/fire_during_onboarding.yaml @@ -18,13 +18,13 @@ tags: text: ".*keep browsing.*" - tapOn: text: "got it" +- assertVisible: + text: ".*browsing activity with the fire button.*" - tapOn: id: "com.duckduckgo.mobile.android:id/fireIconImageView" -- assertVisible: "Personal data can build up in your browser.*" - tapOn: "Cancel" -- assertNotVisible: "Personal data can build up in your browser.*" +- assertNotVisible: ".*browsing activity with the Fire Button.*" - tapOn: id: "com.duckduckgo.mobile.android:id/fireIconImageView" -- assertNotVisible: "Personal data can build up in your browser.*" - tapOn: "Clear All Tabs And Data" - assertVisible: "You've got this!.*" \ No newline at end of file diff --git a/.maestro/notifications_permissions_android13_plus/1_-_permissions_allowed.yaml b/.maestro/notifications_permissions_android13_plus/1_-_permissions_allowed.yaml index 9b689829a654..e77d22ea7d72 100644 --- a/.maestro/notifications_permissions_android13_plus/1_-_permissions_allowed.yaml +++ b/.maestro/notifications_permissions_android13_plus/1_-_permissions_allowed.yaml @@ -11,15 +11,18 @@ appId: com.duckduckgo.mobile.android text: ".*Welcome to DuckDuckGo!.*" optional: true - assertVisible: - text: ".*Not to worry! Searching and browsing privately.*" + text: ".*Ready for a better, more private internet?.*" - tapOn: "let's do it!" +- assertVisible: + text: ".*Privacy protections activated.*" +- tapOn: "choose your browser" - tapOn: "cancel" - assertVisible: - text: ".*I'll also upgrade the security of your connection if possible.*" + text: ".*Try a search!.*" - tapOn: - id: "com.duckduckgo.mobile.android:id/browserMenuImageView" + id: "com.duckduckgo.mobile.android:id/browserMenuImageView" - tapOn: - text: "Downloads" + text: "Downloads" - assertVisible: text: ".*No files downloaded yet.*" - assertNotVisible: diff --git a/.maestro/notifications_permissions_android13_plus/2_-_permissions_denied.yaml b/.maestro/notifications_permissions_android13_plus/2_-_permissions_denied.yaml index 3385855ae1f3..fa3f2d62cdb2 100644 --- a/.maestro/notifications_permissions_android13_plus/2_-_permissions_denied.yaml +++ b/.maestro/notifications_permissions_android13_plus/2_-_permissions_denied.yaml @@ -11,20 +11,23 @@ appId: com.duckduckgo.mobile.android text: ".*Welcome to DuckDuckGo!.*" optional: true - assertVisible: - text: ".*Not to worry! Searching and browsing privately.*" + text: ".*Ready for a better, more private internet?.*" - tapOn: "let's do it!" +- assertVisible: + text: ".*Privacy protections activated.*" +- tapOn: "choose your browser" - tapOn: "cancel" - assertVisible: - text: ".*I'll also upgrade the security of your connection if possible.*" + text: ".*Try a search!.*" - tapOn: - id: "com.duckduckgo.mobile.android:id/browserMenuImageView" + id: "com.duckduckgo.mobile.android:id/browserMenuImageView" - tapOn: - text: "Downloads" + text: "Downloads" - assertVisible: - text: ".*Find out when downloads are ready.*" + text: ".*Find out when downloads are ready.*" - assertVisible: - text: ".*Get a notification when downloads complete.*" + text: ".*Get a notification when downloads complete.*" - assertVisible: - text: ".*Notify Me.*" + text: ".*Notify Me.*" - assertVisible: - text: ".*No files downloaded yet.*" + text: ".*No files downloaded yet.*" diff --git a/.maestro/privacy_tests/10_-_Query_Parameters,_utm_source_and_1_standard_parameter.yaml b/.maestro/privacy_tests/10_-_Query_Parameters,_utm_source_and_1_standard_parameter.yaml index dca7184ce425..b9fdf8a9c1ef 100644 --- a/.maestro/privacy_tests/10_-_Query_Parameters,_utm_source_and_1_standard_parameter.yaml +++ b/.maestro/privacy_tests/10_-_Query_Parameters,_utm_source_and_1_standard_parameter.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: text: "Link with utm_source and 1 standard parameter" - tapOn: diff --git a/.maestro/privacy_tests/11_-_Query_Parameters,_utm_source_and_utm_medium.yaml b/.maestro/privacy_tests/11_-_Query_Parameters,_utm_source_and_utm_medium.yaml index 8396ddd457d0..cf82b9c50670 100644 --- a/.maestro/privacy_tests/11_-_Query_Parameters,_utm_source_and_utm_medium.yaml +++ b/.maestro/privacy_tests/11_-_Query_Parameters,_utm_source_and_utm_medium.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: text: "Link with utm_source and utm_medium" - tapOn: diff --git a/.maestro/privacy_tests/12_-_Query_Parameters,_fbclid,_fb_source_and_1_standard_parameter.yaml b/.maestro/privacy_tests/12_-_Query_Parameters,_fbclid,_fb_source_and_1_standard_parameter.yaml index c50489dee67c..4f83e6a142f4 100644 --- a/.maestro/privacy_tests/12_-_Query_Parameters,_fbclid,_fb_source_and_1_standard_parameter.yaml +++ b/.maestro/privacy_tests/12_-_Query_Parameters,_fbclid,_fb_source_and_1_standard_parameter.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: text: "Link with fbclid, fb_source and 1 standard parameter" - tapOn: diff --git a/.maestro/privacy_tests/13_-_Query_Parameters,_link_which_should_not_be_rewritten.yaml b/.maestro/privacy_tests/13_-_Query_Parameters,_link_which_should_not_be_rewritten.yaml index 738642978237..b13c29934dd9 100644 --- a/.maestro/privacy_tests/13_-_Query_Parameters,_link_which_should_not_be_rewritten.yaml +++ b/.maestro/privacy_tests/13_-_Query_Parameters,_link_which_should_not_be_rewritten.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: text: "Link which should not be rewritten" - tapOn: diff --git a/.maestro/privacy_tests/1_-_Single-site,_single-tab,_session.yaml b/.maestro/privacy_tests/1_-_Single-site,_single-tab,_session.yaml index 0bbc2a9d852b..ed353acc275f 100644 --- a/.maestro/privacy_tests/1_-_Single-site,_single-tab,_session.yaml +++ b/.maestro/privacy_tests/1_-_Single-site,_single-tab,_session.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/2_-_Single-site,_new-tab,_session.yaml b/.maestro/privacy_tests/2_-_Single-site,_new-tab,_session.yaml index 0c3f7c17eb1d..513cde79094d 100644 --- a/.maestro/privacy_tests/2_-_Single-site,_new-tab,_session.yaml +++ b/.maestro/privacy_tests/2_-_Single-site,_new-tab,_session.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - longPressOn: diff --git a/.maestro/privacy_tests/3_-_Single-site,_new-tab,_session_variant_two.yaml b/.maestro/privacy_tests/3_-_Single-site,_new-tab,_session_variant_two.yaml index 89faea769b61..3df9866959f8 100644 --- a/.maestro/privacy_tests/3_-_Single-site,_new-tab,_session_variant_two.yaml +++ b/.maestro/privacy_tests/3_-_Single-site,_new-tab,_session_variant_two.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/4_-_Single-site,_multi-tab_session.yaml b/.maestro/privacy_tests/4_-_Single-site,_multi-tab_session.yaml index 23a7afbf2dbb..7d1cf8490672 100644 --- a/.maestro/privacy_tests/4_-_Single-site,_multi-tab_session.yaml +++ b/.maestro/privacy_tests/4_-_Single-site,_multi-tab_session.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/5_-_Multi-site,_single-tab,_session.yaml b/.maestro/privacy_tests/5_-_Multi-site,_single-tab,_session.yaml index 32691fbaeb5d..8b82d4288268 100644 --- a/.maestro/privacy_tests/5_-_Multi-site,_single-tab,_session.yaml +++ b/.maestro/privacy_tests/5_-_Multi-site,_single-tab,_session.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/6_-_Multi-tab.yaml b/.maestro/privacy_tests/6_-_Multi-tab.yaml index 02152f84d979..e82e3c8eb39f 100644 --- a/.maestro/privacy_tests/6_-_Multi-tab.yaml +++ b/.maestro/privacy_tests/6_-_Multi-tab.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/7_-_Browser_restart_mid-session.yaml b/.maestro/privacy_tests/7_-_Browser_restart_mid-session.yaml index 4a8d5be3184f..762e819d4cdb 100644 --- a/.maestro/privacy_tests/7_-_Browser_restart_mid-session.yaml +++ b/.maestro/privacy_tests/7_-_Browser_restart_mid-session.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/8_-_Navigation_with_back_forward.yaml b/.maestro/privacy_tests/8_-_Navigation_with_back_forward.yaml index f62dbfbe56cb..6008c45fea9c 100644 --- a/.maestro/privacy_tests/8_-_Navigation_with_back_forward.yaml +++ b/.maestro/privacy_tests/8_-_Navigation_with_back_forward.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/privacy_tests/9_-_Navigation_with_refresh.yaml b/.maestro/privacy_tests/9_-_Navigation_with_refresh.yaml index a804f5fbde05..e8767fa87122 100644 --- a/.maestro/privacy_tests/9_-_Navigation_with_refresh.yaml +++ b/.maestro/privacy_tests/9_-_Navigation_with_refresh.yaml @@ -9,12 +9,6 @@ tags: - pressKey: Enter - assertVisible: text: ".*Got It.*" -- tapOn: - text: "HIDE" -- assertVisible: - text: "HIDE TIPS FOREVER" -- tapOn: - text: "HIDE TIPS FOREVER" - assertVisible: id: "ad-id-5" - tapOn: diff --git a/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml b/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml index 3f64af8b6dcf..f5e8af78fce1 100644 --- a/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml +++ b/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml @@ -15,7 +15,7 @@ tags: - extendedWaitUntil: notVisible: "Not DDG." # Spoofed content not visible timeout: 10000 -- tapOn: "Phew!" +- tapOn: "Got it!" - copyTextFrom: id: "omnibarTextInput" - assertTrue: ${maestro.copiedText == "about:blank" || maestro.copiedText == "https://duckduckgo.com/"} \ No newline at end of file diff --git a/.maestro/shared/onboarding.yaml b/.maestro/shared/onboarding.yaml index 666f1d9d144b..104804220109 100644 --- a/.maestro/shared/onboarding.yaml +++ b/.maestro/shared/onboarding.yaml @@ -1,8 +1,11 @@ appId: com.duckduckgo.mobile.android --- - assertVisible: - text: ".*Not to worry! Searching and browsing privately.*" + text: ".*Ready for a better, more private internet?.*" - tapOn: "let's do it!" +- assertVisible: + text: ".*Privacy protections activated.*" +- tapOn: "choose your browser" - tapOn: "cancel" - assertVisible: - text: ".*I'll also upgrade the security of your connection if possible.*" + text: ".*Try a search!.*" diff --git a/.maestro/tabs/open_multiple_tabs.yaml b/.maestro/tabs/open_multiple_tabs.yaml index a558e07a1eb1..90b6b871aeac 100644 --- a/.maestro/tabs/open_multiple_tabs.yaml +++ b/.maestro/tabs/open_multiple_tabs.yaml @@ -6,12 +6,9 @@ tags: - launchApp: clearState: true stopApp: true -- assertVisible: - text: ".*Not to worry! Searching and browsing privately.*" -- tapOn: "let's do it!" -- tapOn: "cancel" -- assertVisible: - text: ".*I'll also upgrade the security of your connection if possible.*" + +- runFlow: ../shared/onboarding.yaml + - tapOn: text: "search or type URL" - inputText: "https://privacy-test-pages.site" diff --git a/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt b/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt index bb5c2d1ab3da..3ee91fb31ff3 100644 --- a/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt +++ b/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt @@ -20,21 +20,18 @@ import android.content.Context import android.util.Log import androidx.lifecycle.LifecycleOwner import com.duckduckgo.app.browser.customtabs.CustomTabDetector -import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.lifecycle.VpnProcessLifecycleObserver import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.appbuildconfig.api.isInternalBuild -import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.checkMainThread import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.library.loader.LibraryLoader +import com.duckduckgo.library.loader.LibraryLoader.LibraryLoaderListener import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import logcat.LogPriority.ERROR import logcat.asLog import logcat.logcat @@ -49,33 +46,21 @@ import logcat.logcat ) @SingleInstanceIn(AppScope::class) class NativeCrashInit @Inject constructor( - context: Context, + private val context: Context, @IsMainProcess private val isMainProcess: Boolean, private val customTabDetector: CustomTabDetector, private val appBuildConfig: AppBuildConfig, private val nativeCrashFeature: NativeCrashFeature, - private val dispatcherProvider: DispatcherProvider, - @AppCoroutineScope private val coroutineScope: CoroutineScope, -) : MainProcessLifecycleObserver, VpnProcessLifecycleObserver { +) : MainProcessLifecycleObserver, VpnProcessLifecycleObserver, LibraryLoaderListener { private val isCustomTab: Boolean by lazy { customTabDetector.isCustomTab() } private val processName: String by lazy { if (isMainProcess) "main" else "vpn" } - init { - try { - LibraryLoader.loadLibrary(context, "crash-ndk") - } catch (ignored: Throwable) { - logcat(ERROR) { "ndk-crash: Error loading crash-ndk lib: ${ignored.asLog()}" } - } - } - private external fun jni_register_sighandler(logLevel: Int, appVersion: String, processName: String, isCustomTab: Boolean) override fun onCreate(owner: LifecycleOwner) { if (isMainProcess) { - coroutineScope.launch { - jniRegisterNativeSignalHandler() - } + asyncLoadNativeLibrary() } else { logcat(ERROR) { "ndk-crash: onCreate wrongly called in a secondary process" } } @@ -83,18 +68,21 @@ class NativeCrashInit @Inject constructor( override fun onVpnProcessCreated() { if (!isMainProcess) { - coroutineScope.launch { - jniRegisterNativeSignalHandler() - } + asyncLoadNativeLibrary() } else { logcat(ERROR) { "ndk-crash: onCreate wrongly called in the main process" } } } - private suspend fun jniRegisterNativeSignalHandler() = withContext(dispatcherProvider.io()) { + override fun success() { + // do not call on main thread + checkMainThread() + runCatching { - if (isMainProcess && !nativeCrashFeature.nativeCrashHandling().isEnabled()) return@withContext - if (!isMainProcess && !nativeCrashFeature.nativeCrashHandlingSecondaryProcess().isEnabled()) return@withContext + logcat(ERROR) { "ndk-crash: Library loaded in process $processName" } + + if (isMainProcess && !nativeCrashFeature.nativeCrashHandling().isEnabled()) return + if (!isMainProcess && !nativeCrashFeature.nativeCrashHandlingSecondaryProcess().isEnabled()) return val logLevel = if (appBuildConfig.isDebug || appBuildConfig.isInternalBuild()) { Log.VERBOSE @@ -106,4 +94,12 @@ class NativeCrashInit @Inject constructor( logcat(ERROR) { "ndk-crash: Error calling jni_register_sighandler: ${it.asLog()}" } } } + + override fun failure(t: Throwable?) { + logcat(ERROR) { "ndk-crash: error loading library in process $processName: ${t?.asLog()}" } + } + + private fun asyncLoadNativeLibrary() { + LibraryLoader.loadLibrary(context, "crash-ndk", this) + } } diff --git a/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesActivePluginPointCodeGenerator.kt b/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesActivePluginPointCodeGenerator.kt index 07c7b7874246..a600240933ca 100644 --- a/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesActivePluginPointCodeGenerator.kt +++ b/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesActivePluginPointCodeGenerator.kt @@ -49,6 +49,7 @@ import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asClassName import dagger.Binds import java.io.File +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.name.FqName @@ -123,6 +124,16 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator { val pluginClassType = vmClass.pluginClassName(ContributesActivePluginPoint::class.fqName) ?: vmClass.asClassName() val featureName = "pluginPoint${pluginClassType.simpleName}" + // Check if there's another plugin point class that has the same class simplename + // we can't allow that because the backing remote feature would be the same + val existingFeature = featureBackedClassNames.putIfAbsent(featureName, vmClass.fqName) + if (existingFeature != null) { + throw AnvilCompilationException( + "${vmClass.fqName} plugin point naming is duplicated, previous found in $existingFeature", + element = vmClass.clazz.identifyingElement, + ) + } + val content = FileSpec.buildFile(generatedPackage, pluginPointClassFileName) { // This is the normal plugin point addType( @@ -304,6 +315,16 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator { val pluginRemoteFeatureStoreClassName = "${vmClass.shortName}_ActivePlugin_RemoteFeature_MultiProcessStore" val pluginPriority = vmClass.annotations.firstOrNull { it.fqName == ContributesActivePlugin::class.fqName }?.priorityOrNull() + // Check if there's another plugin class, in the same plugin point, that has the same class simplename + // we can't allow that because the backing remote feature would be the same + val existingFeature = featureBackedClassNames.putIfAbsent("${featureName}_$parentFeatureName", vmClass.fqName) + if (existingFeature != null) { + throw AnvilCompilationException( + "${vmClass.fqName} plugin name is duplicated, previous found in $existingFeature", + element = vmClass.clazz.identifyingElement, + ) + } + val content = FileSpec.buildFile(generatedPackage, pluginClassName) { // First create the class that will contribute the active plugin. // We do expect that the plugins are define using the "ContributesActivePlugin" annotation but are also injected @@ -570,6 +591,8 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator { } companion object { + internal val featureBackedClassNames = ConcurrentHashMap() + private val pluginPointFqName = FqName("com.duckduckgo.common.utils.plugins.PluginPoint") private val dispatcherProviderFqName = FqName("com.duckduckgo.common.utils.DispatcherProvider") private val activePluginPointFqName = FqName("com.duckduckgo.common.utils.plugins.InternalActivePluginPoint") diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index cdac091eba15..fc044cb3e26c 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -66,7 +66,7 @@ import com.duckduckgo.app.browser.camera.CameraHardwareChecker import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository import com.duckduckgo.app.browser.certificates.remoteconfig.SSLCertificatesFeature import com.duckduckgo.app.browser.commands.Command -import com.duckduckgo.app.browser.commands.Command.HideExperimentOnboardingDialog +import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxDialog import com.duckduckgo.app.browser.commands.Command.LaunchPrivacyPro import com.duckduckgo.app.browser.commands.Command.LoadExtractedUrl import com.duckduckgo.app.browser.commands.Command.ShowBackNavigationHistory @@ -104,10 +104,8 @@ import com.duckduckgo.app.cta.model.DismissedCta import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta -import com.duckduckgo.app.cta.ui.DaxDialogCta -import com.duckduckgo.app.cta.ui.ExperimentDaxBubbleOptionsCta -import com.duckduckgo.app.cta.ui.ExperimentOnboardingDaxDialogCta import com.duckduckgo.app.cta.ui.HomePanelCta +import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepositoryImpl @@ -126,8 +124,8 @@ import com.duckduckgo.app.location.data.LocationPermissionsRepositoryImpl import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager -import com.duckduckgo.app.onboarding.ui.page.experiment.OnboardingExperimentPixel +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao @@ -443,7 +441,7 @@ class BrowserTabViewModelTest { private val androidBrowserConfig: AndroidBrowserConfigFeature = mock() - private val mockToggle: Toggle = mock() + private val mockEnabledToggle: Toggle = mock { on { it.isEnabled() } doReturn true } private val mockPrivacyProtectionsPopupManager: PrivacyProtectionsPopupManager = mock() @@ -458,7 +456,7 @@ class BrowserTabViewModelTest { private val mockFaviconFetchingPrompt: FaviconsFetchingPrompt = mock() private val mockSSLCertificatesFeature: SSLCertificatesFeature = mock() private val mockBypassedSSLCertificatesRepository: BypassedSSLCertificatesRepository = mock() - private val mockExtendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager = mock() + private val mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock() private val mockUserBrowserProperties: UserBrowserProperties = mock() @Before @@ -485,9 +483,9 @@ class BrowserTabViewModelTest { whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(bookmarksListFlow.consumeAsFlow()) whenever(mockRemoteMessagingRepository.messageFlow()).thenReturn(remoteMessageFlow.consumeAsFlow()) whenever(mockSettingsDataStore.automaticFireproofSetting).thenReturn(AutomaticFireproofSetting.ASK_EVERY_TIME) - whenever(androidBrowserConfig.screenLock()).thenReturn(mockToggle) - whenever(mockSSLCertificatesFeature.allowBypass()).thenReturn(mockToggle) - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(false) + whenever(androidBrowserConfig.screenLock()).thenReturn(mockEnabledToggle) + whenever(mockSSLCertificatesFeature.allowBypass()).thenReturn(mockEnabledToggle) + whenever(mockExtendedOnboardingFeatureToggles.aestheticUpdates()).thenReturn(mockEnabledToggle) whenever(subscriptions.shouldLaunchPrivacyProForUrl(any())).thenReturn(false) remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider) @@ -505,7 +503,7 @@ class BrowserTabViewModelTest { dispatchers = coroutineRule.testDispatcherProvider, duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(), surveyRepository = mockSurveyRepository, - extendedOnboardingExperimentVariantManager = mockExtendedOnboardingExperimentVariantManager, + extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles, ) val siteFactory = SiteFactoryImpl( @@ -597,7 +595,7 @@ class BrowserTabViewModelTest { subscriptions = subscriptions, sslCertificatesFeature = mockSSLCertificatesFeature, bypassedSSLCertificatesRepository = mockBypassedSSLCertificatesRepository, - extendedOnboardingExperimentVariantManager = mockExtendedOnboardingExperimentVariantManager, + extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles, userBrowserProperties = mockUserBrowserProperties, ) @@ -2345,7 +2343,7 @@ class BrowserTabViewModelTest { @Test fun whenRegisterDaxBubbleCtaDismissedThenRegisterInDatabase() = runTest { - val cta = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) + val cta = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) testee.ctaViewState.value = CtaViewState(cta = cta) testee.registerDaxBubbleCtaDismissed() @@ -2354,7 +2352,7 @@ class BrowserTabViewModelTest { @Test fun whenRegisterDaxBubbleCtaDismissedThenCtaChangedToNull() = runTest { - val cta = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) + val cta = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) testee.ctaViewState.value = CtaViewState(cta = cta) testee.registerDaxBubbleCtaDismissed() @@ -2371,7 +2369,7 @@ class BrowserTabViewModelTest { @Test fun whenUserClickedCtaButtonThenFirePixel() { - val cta = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) + val cta = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) setCta(cta) testee.onUserClickCtaOkButton() verify(mockPixel).fire(cta.okPixel!!, cta.pixelOkParameters()) @@ -2431,32 +2429,6 @@ class BrowserTabViewModelTest { verify(mockPixel).fire(cta.cancelPixel!!, cta.pixelCancelParameters()) } - @Test - fun whenUserClickedHideDaxDialogThenHideDaxDialogCommandSent() { - val cta = DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) - setCta(cta) - testee.onUserHideDaxDialog() - val command = captureCommands().lastValue - assertTrue(command is Command.DaxCommand.HideDaxDialog) - } - - @Test - fun whenUserDismissDaxTrackersBlockedDialogThenFinishTrackerAnimationCommandSent() { - val cta = DaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList(), "") - setCta(cta) - testee.onDaxDialogDismissed() - val command = captureCommands().lastValue - assertTrue(command is Command.DaxCommand.FinishPartialTrackerAnimation) - } - - @Test - fun whenUserDismissDifferentThanDaxTrackersBlockedDialogThenFinishTrackerAnimationCommandNotSent() { - val cta = DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) - setCta(cta) - testee.onDaxDialogDismissed() - assertCommandNotIssued() - } - @Test fun whenUserDismissedCtaThenRegisterInDatabase() = runTest { val cta = HomePanelCta.AddWidgetAuto @@ -4881,14 +4853,14 @@ class BrowserTabViewModelTest { @Test fun whenProcessJsCallbackMessageScreenLockNotEnabledDoNotSendCommand() = runTest { - whenever(mockToggle.isEnabled()).thenReturn(false) + whenever(mockEnabledToggle.isEnabled()).thenReturn(false) testee.processJsCallbackMessage("myFeature", "screenLock", "myId", JSONObject("""{ "my":"object"}""")) assertCommandNotIssued() } @Test fun whenProcessJsCallbackMessageScreenLockEnabledSendCommand() = runTest { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) testee.processJsCallbackMessage("myFeature", "screenLock", "myId", JSONObject("""{ "my":"object"}""")) assertCommandIssued { assertEquals("object", this.data.params.getString("my")) @@ -4900,14 +4872,14 @@ class BrowserTabViewModelTest { @Test fun whenProcessJsCallbackMessageScreenUnlockNotEnabledDoNotSendCommand() = runTest { - whenever(mockToggle.isEnabled()).thenReturn(false) + whenever(mockEnabledToggle.isEnabled()).thenReturn(false) testee.processJsCallbackMessage("myFeature", "screenUnlock", "myId", JSONObject("""{ "my":"object"}""")) assertCommandNotIssued() } @Test fun whenProcessJsCallbackMessageScreenUnlockEnabledSendCommand() = runTest { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) testee.processJsCallbackMessage("myFeature", "screenUnlock", "myId", JSONObject("""{ "my":"object"}""")) assertCommandIssued() } @@ -5075,7 +5047,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenAllowBypassSSLCertificatesFeatureDisabledThenSSLCertificateErrorsAreIgnored() { - whenever(mockToggle.isEnabled()).thenReturn(false) + whenever(mockEnabledToggle.isEnabled()).thenReturn(false) val url = "http://example.com" givenCurrentSite(url) @@ -5090,7 +5062,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenSslCertificateIssueReceivedForLoadingSiteThenShowSslWarningCommandSentAndViewStatesUpdated() { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) val url = "http://example.com" givenCurrentSite(url) @@ -5111,7 +5083,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenSslCertificateIssueReceivedForAnotherSiteThenShowSslWarningCommandNotSentAndViewStatesNotUpdated() { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) val url = "http://example.com" givenCurrentSite(url) @@ -5127,7 +5099,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenInFreshStartAndSslCertificateIssueReceivedThenShowSslWarningCommandSentAndViewStatesUpdated() = runTest { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) val url = "http://example.com" val site: Site = mock() whenever(site.url).thenReturn(url) @@ -5196,7 +5168,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenSslCertificateActionProceedThenPixelsFiredAndViewStatesUpdated() { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) val url = "http://example.com" val certificate = aRSASslCertificate() val sslErrorResponse = SslErrorResponse(SslError(SslError.SSL_EXPIRED, certificate, url), EXPIRED, url) @@ -5215,7 +5187,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenWebViewRefreshedThenSSLErrorStateIsNone() { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) val url = "http://example.com" val certificate = aRSASslCertificate() val sslErrorResponse = SslErrorResponse(SslError(SslError.SSL_EXPIRED, certificate, url), EXPIRED, url) @@ -5230,7 +5202,7 @@ class BrowserTabViewModelTest { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun whenResetSSLErrorThenBrowserErrorStateIsLoading() { - whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) val url = "http://example.com" val certificate = aRSASslCertificate() val sslErrorResponse = SslErrorResponse(SslError(SslError.SSL_EXPIRED, certificate, url), EXPIRED, url) @@ -5287,42 +5259,18 @@ class BrowserTabViewModelTest { } @Test - fun whenOnboardingExperimentEnabledThenSetBrowserBackgroundWithNewDrawable() = runTest { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) - - testee.configureBrowserBackground() - - assertCommandIssued { - assertEquals(R.drawable.onboarding_experiment_background_bitmap, this.backgroundRes) - } - } - - @Test - fun whenOnboardingExperimentDisabledThenSetBrowserBackgroundWithNoDrawable() = runTest { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(false) - - testee.configureBrowserBackground() - - assertCommandIssued { - assertEquals(0, this.backgroundRes) - } - } - - @Test - fun givenOnboardingExperimentEnabledWhenTrackersBlockedCtaShownThenPrivacyShieldIsHighlighted() = runTest { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) - val cta = ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList()) + fun whenTrackersBlockedCtaShownThenPrivacyShieldIsHighlighted() = runTest { + val cta = OnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList()) testee.ctaViewState.value = ctaViewState().copy(cta = cta) - testee.onExperimentDaxTypingAnimationFinished() + testee.onOnboardingDaxTypingAnimationFinished() assertTrue(browserViewState().showPrivacyShield.isHighlighted()) } @Test - fun givenOnboardingExperimentEnabledAndPrivacyShieldHighlightedWhenShieldIconSelectedThenStopPulse() = runTest { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) - val cta = ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList()) + fun givenPrivacyShieldHighlightedWhenShieldIconSelectedThenStopPulse() = runTest { + val cta = OnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList()) testee.ctaViewState.value = ctaViewState().copy(cta = cta) testee.onPrivacyShieldSelected() @@ -5330,9 +5278,8 @@ class BrowserTabViewModelTest { } @Test - fun givenOnboardingExperimentEnabledAndPrivacyShieldHighlightedWhenShieldIconSelectedThenSendPixel() = runTest { + fun givenPrivacyShieldHighlightedWhenShieldIconSelectedThenSendPixel() = runTest { whenever(mockUserBrowserProperties.daysSinceInstalled()).thenReturn(0) - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) testee.browserViewState.value = browserViewState().copy(showPrivacyShield = HighlightableButton.Visible(highlighted = true)) val testParams = mapOf("daysSinceInstall" to "0", "from_onboarding" to "true") @@ -5341,33 +5288,31 @@ class BrowserTabViewModelTest { } @Test - fun givenOnboardingExperimentEnabledWhenUserDismissDaxTrackersBlockedDialogThenFinishPrivacyShieldPulse() { - val cta = ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList()) + fun whenUserDismissDaxTrackersBlockedDialogThenFinishPrivacyShieldPulse() { + val cta = OnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, emptyList()) setCta(cta) - testee.onDaxDialogDismissed() + testee.onUserDismissedCta() assertFalse(browserViewState().showPrivacyShield.isHighlighted()) } @Test - fun givenOnboardingExperimentCtaShownWhenUserSubmittedQueryThenDismissCta() { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) + fun givenOnboardingCtaShownWhenUserSubmittedQueryThenDismissCta() { whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com") - val cta = ExperimentOnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) + val cta = OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) testee.ctaViewState.value = CtaViewState(cta = cta) testee.onUserSubmittedQuery("foo") - assertCommandIssued { - assertEquals(cta, this.experimentCta) + assertCommandIssued { + assertEquals(cta, this.onboardingCta) } } @Test fun givenSuggestedSearchesDialogShownWhenUserSubmittedQueryThenCustomSearchPixelIsSent() { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com") - val cta = ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) + val cta = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) testee.ctaViewState.value = CtaViewState(cta = cta) testee.onUserSubmittedQuery("foo") @@ -5375,22 +5320,10 @@ class BrowserTabViewModelTest { verify(mockPixel).fire(OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_CUSTOM) } - @Test - fun givenSuggestedSearchesDialogShownWhenUserSubmittedQueryDifferentFromOptionsThenPixelIsNotSent() { - whenever(mockOmnibarConverter.convertQueryToUrl("mighty ducks cast", null)).thenReturn("mighty ducks cast") - val cta = ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) - testee.ctaViewState.value = CtaViewState(cta = cta) - - testee.onUserSubmittedQuery("mighty ducks cast") - - verify(mockPixel, never()).fire(OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_CUSTOM) - } - @Test fun givenSuggestedSitesDialogShownWhenUserSubmittedQueryThenCustomSitePixelIsSent() { - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com") - val cta = ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroVisitSiteOptionsCta(mockOnboardingStore, mockAppInstallStore) + val cta = DaxBubbleCta.DaxIntroVisitSiteOptionsCta(mockOnboardingStore, mockAppInstallStore) testee.ctaViewState.value = CtaViewState(cta = cta) testee.onUserSubmittedQuery("foo") diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 65523b8e763b..2aabae8b2edb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -32,7 +32,7 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles import com.duckduckgo.app.pixels.AppPixelName.* import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.app.privacy.model.HttpsStatus @@ -52,6 +52,7 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule +import com.duckduckgo.feature.toggles.api.Toggle import java.util.concurrent.TimeUnit import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.drop @@ -115,7 +116,7 @@ class CtaViewModelTest { private lateinit var mockSurveyRepository: SurveyRepository @Mock - private lateinit var mockExtendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager + private lateinit var mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles private val requiredDaxOnboardingCtas: List = listOf( CtaId.DAX_INTRO, @@ -137,11 +138,12 @@ class CtaViewModelTest { .allowMainThreadQueries() .build() + val mockToggle: Toggle = mock { on { it.isEnabled() } doReturn true } + whenever(mockExtendedOnboardingFeatureToggles.aestheticUpdates()).thenReturn(mockToggle) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) whenever(mockUserAllowListRepository.isDomainInUserAllowList(any())).thenReturn(false) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(db.dismissedCtaDao().dismissedCtas()) whenever(mockTabRepository.flowTabs).thenReturn(db.tabsDao().flowTabs()) - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(false) testee = CtaViewModel( appInstallStore = mockAppInstallStore, @@ -156,7 +158,7 @@ class CtaViewModelTest { dispatchers = coroutineRule.testDispatcherProvider, duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(), surveyRepository = mockSurveyRepository, - extendedOnboardingExperimentVariantManager = mockExtendedOnboardingExperimentVariantManager, + extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles, ) } @@ -202,7 +204,7 @@ class CtaViewModelTest { @Test fun whenCtaShownAndCtaIsDaxAndCanNotSendPixelThenPixelIsNotFired() { - testee.onCtaShown(DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore)) + testee.onCtaShown(DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore)) verify(mockPixel, never()).fire(eq(SURVEY_CTA_SHOWN), any(), any(), eq(COUNT)) } @@ -256,32 +258,20 @@ class CtaViewModelTest { fun whenCtaDismissedAndAllDaxOnboardingCtasShownThenStageCompleted() = runTest { givenDaxOnboardingActive() givenShownDaxOnboardingCtas(requiredDaxOnboardingCtas) - testee.onUserDismissedCta(DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore)) + testee.onUserDismissedCta(OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore)) verify(mockUserStageStore).stageCompleted(AppStage.DAX_ONBOARDING) } @Test - fun whenHideTipsForeverThenPixelIsFired() = runTest { - testee.hideTipsForever(HomePanelCta.AddWidgetAuto) - verify(mockPixel).fire(eq(ONBOARDING_DAX_ALL_CTA_HIDDEN), any(), any(), eq(COUNT)) - } - - @Test - fun whenHideTipsForeverThenHideTipsSetToTrueOnSettings() = runTest { - testee.hideTipsForever(HomePanelCta.AddWidgetAuto) - verify(mockSettingsDataStore).hideTips = true - } - - @Test - fun whenHideTipsForeverThenDaxOnboardingStageCompleted() = runTest { - testee.hideTipsForever(HomePanelCta.AddWidgetAuto) - verify(mockUserStageStore).stageCompleted(AppStage.DAX_ONBOARDING) + fun whenRegisterDaxBubbleIntroCtaThenDatabaseNotified() = runTest { + testee.registerDaxBubbleCtaDismissed(DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore)) + verify(mockDismissedCtaDao).insert(DismissedCta(CtaId.DAX_INTRO)) } @Test - fun whenRegisterDaxBubbleIntroCtaThenDatabaseNotified() = runTest { - testee.registerDaxBubbleCtaDismissed(DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore)) - verify(mockDismissedCtaDao).insert(DismissedCta(CtaId.DAX_INTRO)) + fun whenRegisterDaxBubbleIntroVisitSiteCtaThenDatabaseNotified() = runTest { + testee.registerDaxBubbleCtaDismissed(DaxBubbleCta.DaxIntroVisitSiteOptionsCta(mockOnboardingStore, mockAppInstallStore)) + verify(mockDismissedCtaDao).insert(DismissedCta(CtaId.DAX_INTRO_VISIT_SITE)) } @Test @@ -371,10 +361,10 @@ class CtaViewModelTest { "Facebook", ) - val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) as DaxDialogCta + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) as OnboardingDaxDialogCta - assertTrue(value is DaxDialogCta.DaxMainNetworkCta) - val actualText = (value as DaxDialogCta.DaxMainNetworkCta).getDaxText(context) + assertTrue(value is OnboardingDaxDialogCta.DaxMainNetworkCta) + val actualText = (value as OnboardingDaxDialogCta.DaxMainNetworkCta).getTrackersDescription(context) assertEquals(expectedCtaText, actualText) } @@ -382,7 +372,7 @@ class CtaViewModelTest { fun whenRefreshCtaWhileBrowsingOnSiteOwnedByMajorTrackerThenReturnNetworkCta() = runTest { givenDaxOnboardingActive() val site = site(url = "http://m.instagram.com", entity = TestEntity("Facebook", "Facebook", 9.0)) - val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) as DaxDialogCta + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) as OnboardingDaxDialogCta val expectedCtaText = context.resources.getString( R.string.daxMainNetworkOwnedCtaText, "Facebook", @@ -390,8 +380,8 @@ class CtaViewModelTest { "Facebook", ) - assertTrue(value is DaxDialogCta.DaxMainNetworkCta) - val actualText = (value as DaxDialogCta.DaxMainNetworkCta).getDaxText(context) + assertTrue(value is OnboardingDaxDialogCta.DaxMainNetworkCta) + val actualText = (value as OnboardingDaxDialogCta.DaxMainNetworkCta).getTrackersDescription(context) assertEquals(expectedCtaText, actualText) } @@ -410,7 +400,7 @@ class CtaViewModelTest { val site = site(url = "http://www.cnn.com", trackerCount = 1, events = listOf(trackingEvent)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - assertTrue(value is DaxDialogCta.DaxTrackersBlockedCta) + assertTrue(value is OnboardingDaxDialogCta.DaxTrackersBlockedCta) } @Test @@ -428,16 +418,16 @@ class CtaViewModelTest { val site = site(url = "http://www.cnn.com", trackerCount = 1, events = listOf(trackingEvent)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - assertTrue(value is DaxDialogCta.DaxTrackersBlockedCta) + assertTrue(value is OnboardingDaxDialogCta.DaxTrackersBlockedCta) } @Test - fun whenRefreshCtaWhileBrowsingAndNoTrackersInformationThenReturnNoSerpCta() = runTest { + fun whenRefreshCtaWhileBrowsingAndNoTrackersInformationThenReturnNoTrackersCta() = runTest { givenDaxOnboardingActive() val site = site(url = "http://www.cnn.com", trackerCount = 1) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - assertTrue(value is DaxDialogCta.DaxNoSerpCta) + assertTrue(value is OnboardingDaxDialogCta.DaxNoTrackersCta) } @Test @@ -446,16 +436,16 @@ class CtaViewModelTest { val site = site(url = "http://www.duckduckgo.com") val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - assertTrue(value is DaxDialogCta.DaxSerpCta) + assertTrue(value is OnboardingDaxDialogCta.DaxSerpCta) } @Test - fun whenRefreshCtaWhileBrowsingThenReturnNoSerpCta() = runTest { + fun whenRefreshCtaWhileBrowsingThenReturnNoTrackersCta() = runTest { givenDaxOnboardingActive() val site = site(url = "http://www.wikipedia.com") val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - assertTrue(value is DaxDialogCta.DaxNoSerpCta) + assertTrue(value is OnboardingDaxDialogCta.DaxNoTrackersCta) } @Test @@ -464,7 +454,7 @@ class CtaViewModelTest { val site = site(url = "http://www.wikipedia.com") val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - assertTrue(value is DaxDialogCta.DaxNoSerpCta) + assertTrue(value is OnboardingDaxDialogCta.DaxNoTrackersCta) } @Test @@ -473,7 +463,7 @@ class CtaViewModelTest { val site = site(url = "http://www.wikipedia.com") val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false, site = site) - assertTrue(value !is DaxDialogCta) + assertFalse(value is OnboardingDaxDialogCta) } @Test @@ -483,13 +473,23 @@ class CtaViewModelTest { whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP)).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) - assertTrue(value is DaxBubbleCta.DaxIntroCta) + assertTrue(value is DaxBubbleCta.DaxIntroSearchOptionsCta) } @Test - fun whenRefreshCtaOnHomeTabAndIntroCtaWasShownThenEndCtaShown() = runTest { + fun whenRefreshCtaOnHomeTabAndIntroCtaWasShownThenVisitSiteCtaShown() = runTest { givenDaxOnboardingActive() whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) + assertTrue(value is DaxBubbleCta.DaxIntroVisitSiteOptionsCta) + } + + @Test + fun whenRefreshCtaOnHomeTabAndIntroAndVisitSiteCtasWereShownThenEndCtaShown() = runTest { + givenDaxOnboardingActive() + whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO_VISIT_SITE)).thenReturn(true) givenAtLeastOneDaxDialogCtaShown() val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) @@ -636,25 +636,6 @@ class CtaViewModelTest { launch.cancel() } - @Test - fun whenFirstTimeUserClicksOnFireButtonThenFireDialogCtaReturned() = runTest { - givenDaxOnboardingActive() - - val fireDialogCta = testee.getFireDialogCta() - - assertTrue(fireDialogCta is DaxFireDialogCta.TryClearDataCta) - } - - @Test - fun whenFirstTimeUserClicksOnFireButtonButUserHidAllTipsThenFireDialogCtaIsNull() = runTest { - givenDaxOnboardingActive() - whenever(mockSettingsDataStore.hideTips).thenReturn(true) - - val fireDialogCta = testee.getFireDialogCta() - - assertNull(fireDialogCta) - } - @Test fun whenFireCtaDismissedThenFireDialogCtaIsNull() = runTest { givenDaxOnboardingActive() @@ -666,20 +647,19 @@ class CtaViewModelTest { } @Test - fun givenExperimentEnabledWhenRefreshCtaOnHomeTabAndIntroCtaWasNotPreviouslyShownThenSearchSuggestionsCtaShown() = runTest { + fun whenRefreshCtaOnHomeTabAndIntroCtaWasNotPreviouslyShownThenSearchSuggestionsCtaShown() = runTest { givenDaxOnboardingActive() whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(false) whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP)).thenReturn(true) - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) - assertTrue(value is ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroSearchOptionsCta) + assertTrue(value is DaxBubbleCta.DaxIntroSearchOptionsCta) } @Test - fun whenRegisterDaxExperimentVisitSiteCtaThenDatabaseNotified() = runTest { + fun whenRegisterDismissedDaxIntroVisitSiteCtaThenDatabaseNotified() = runTest { testee.registerDaxBubbleCtaDismissed( - ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroVisitSiteOptionsCta( + DaxBubbleCta.DaxIntroVisitSiteOptionsCta( mockOnboardingStore, mockAppInstallStore, ), @@ -692,10 +672,9 @@ class CtaViewModelTest { givenDaxOnboardingActive() whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_TRACKERS_FOUND)).thenReturn(false) - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) - assertTrue(value is ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroVisitSiteOptionsCta) + assertTrue(value is DaxBubbleCta.DaxIntroVisitSiteOptionsCta) } @Test @@ -703,10 +682,9 @@ class CtaViewModelTest { givenDaxOnboardingActive() whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_TRACKERS_FOUND)).thenReturn(true) - whenever(mockExtendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) - assertFalse(value is ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroVisitSiteOptionsCta) + assertFalse(value is DaxBubbleCta.DaxIntroVisitSiteOptionsCta) } private suspend fun givenDaxOnboardingActive() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 18494e367606..39da910fb283 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -41,7 +41,6 @@ import com.duckduckgo.app.browser.BrowserViewModel.Command.Query import com.duckduckgo.app.browser.databinding.ActivityBrowserBinding import com.duckduckgo.app.browser.databinding.IncludeOmnibarToolbarMockupBinding import com.duckduckgo.app.browser.shortcut.ShortcutBuilder -import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.downloads.DownloadsActivity import com.duckduckgo.app.feedback.ui.common.FeedbackActivity @@ -56,7 +55,6 @@ import com.duckduckgo.app.global.view.renderIfChanged import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPage import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.FIRE_DIALOG_CANCEL -import com.duckduckgo.app.pixels.AppPixelName.FIRE_DIALOG_PROMOTED_CANCEL import com.duckduckgo.app.playstore.PlayStoreUtils import com.duckduckgo.app.settings.SettingsActivity import com.duckduckgo.app.settings.db.SettingsDataStore @@ -102,9 +100,6 @@ open class BrowserActivity : DuckDuckGoActivity() { @Inject lateinit var dataClearerForegroundAppRestartPixel: DataClearerForegroundAppRestartPixel - @Inject - lateinit var ctaViewModel: CtaViewModel - @Inject lateinit var userEventsStore: UserEventsStore @@ -441,7 +436,6 @@ open class BrowserActivity : DuckDuckGoActivity() { val dialog = FireDialog( context = this, clearPersonalDataAction = clearPersonalDataAction, - ctaViewModel = ctaViewModel, pixel = pixel, settingsDataStore = settingsDataStore, userEventsStore = userEventsStore, @@ -453,7 +447,7 @@ open class BrowserActivity : DuckDuckGoActivity() { } dialog.setOnShowListener { currentTab?.onFireDialogVisibilityChanged(isVisible = true) } dialog.setOnCancelListener { - pixel.fire(if (dialog.ctaVisible) FIRE_DIALOG_PROMOTED_CANCEL else FIRE_DIALOG_CANCEL) + pixel.fire(FIRE_DIALOG_CANCEL) currentTab?.onFireDialogVisibilityChanged(isVisible = false) } dialog.show() diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 6fd29776b4f3..227f232df83a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -166,7 +166,6 @@ import com.duckduckgo.app.browser.webshare.WebShareChooser import com.duckduckgo.app.browser.webview.DummyWebMessageListenerFeature import com.duckduckgo.app.browser.webview.WebContentDebugging import com.duckduckgo.app.cta.ui.* -import com.duckduckgo.app.cta.ui.DaxDialogCta.* import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.website @@ -230,7 +229,6 @@ import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.DuckDuckGoFragment import com.duckduckgo.common.ui.store.BrowserAppTheme import com.duckduckgo.common.ui.view.DaxDialog -import com.duckduckgo.common.ui.view.DaxDialogListener import com.duckduckgo.common.ui.view.KeyboardAwareEditText import com.duckduckgo.common.ui.view.KeyboardAwareEditText.ShowSuggestionsListener import com.duckduckgo.common.ui.view.dialog.ActionBottomSheetDialog @@ -563,11 +561,11 @@ class BrowserTabFragment : private val daxDialogCta get() = binding.includeNewBrowserTab.includeDaxDialogCta - private val daxDialogIntroExperimentCta - get() = binding.includeNewBrowserTab.includeDaxDialogIntroExperimentCta + private val daxDialogIntroBubbleCta + get() = binding.includeNewBrowserTab.includeDaxDialogIntroBubbleCta - private val daxDialogExperimentOnboardingCta - get() = binding.includeOnboardingDaxDialogExperiment + private val daxDialogOnboardingCta + get() = binding.includeOnboardingDaxDialog private val smoothProgressAnimator by lazy { SmoothProgressAnimator(omnibar.pageLoadingIndicator) } @@ -1493,8 +1491,6 @@ class BrowserTabFragment : is Command.GenerateWebViewPreviewImage -> generateWebViewPreviewImage() is Command.LaunchTabSwitcher -> launchTabSwitcher() is Command.ShowErrorWithAction -> showErrorSnackbar(it) - is Command.DaxCommand.FinishPartialTrackerAnimation -> finishPartialTrackerAnimation() - is Command.DaxCommand.HideDaxDialog -> showHideTipsDialog(it.cta) is Command.HideWebContent -> webView?.hide() is Command.ShowWebContent -> webView?.show() is Command.CheckSystemLocationPermission -> checkSystemLocationPermission(it.domain, it.deniedForever) @@ -1545,12 +1541,11 @@ class BrowserTabFragment : is Command.ScreenLock -> screenLock(it.data) is Command.ScreenUnlock -> screenUnlock() is Command.ShowFaviconsPrompt -> showFaviconsPrompt() - is Command.SetBrowserBackground -> setBrowserBackground(it.backgroundRes) is Command.ShowWebPageTitle -> showWebPageTitleInCustomTab(it.title, it.url) is Command.ShowSSLError -> showSSLWarning(it.handler, it.error) is Command.HideSSLError -> hideSSLWarning() is Command.LaunchScreen -> launchScreen(it.screen, it.payload) - is Command.HideExperimentOnboardingDialog -> hideOnboardingDaxDialog(it.experimentCta) + is Command.HideOnboardingDaxDialog -> hideOnboardingDaxDialog(it.onboardingCta) else -> { // NO OP } @@ -2274,8 +2269,7 @@ class BrowserTabFragment : @SuppressLint("SetJavaScriptEnabled") private fun configureWebView() { - viewModel.configureBrowserBackground() - binding.experimentDaxDialogContent.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) + binding.daxDialogOnboardingCtaContent.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) webView = layoutInflater.inflate( R.layout.include_duckduckgo_browser_webview, @@ -2400,11 +2394,7 @@ class BrowserTabFragment : faviconPrompt.show() } - private fun setBrowserBackground(backgroundRes: Int) { - newBrowserTab.browserBackground.setBackgroundResource(backgroundRes) - } - - private fun hideOnboardingDaxDialog(experimentCta: ExperimentOnboardingDaxDialogCta) { + private fun hideOnboardingDaxDialog(experimentCta: OnboardingDaxDialogCta) { experimentCta.hideOnboardingCta(binding) } @@ -2916,7 +2906,6 @@ class BrowserTabFragment : if (newBrowserTab.ctaContainer.isNotEmpty()) { renderer.renderHomeCta() } - renderer.recreateDaxDialogCta() configureQuickAccessGridLayout(quickAccessItems.quickAccessRecyclerView) configureQuickAccessGridLayout(binding.quickAccessSuggestionsRecyclerView) decorator.recreatePopupMenu() @@ -3130,16 +3119,6 @@ class BrowserTabFragment : } } - private fun finishPartialTrackerAnimation() { - animatorHelper.finishPartialTrackerAnimation() - } - - private fun showHideTipsDialog(cta: Cta) { - context?.let { - launchHideTipsDialog(it, cta) - } - } - private fun showBackNavigationHistory(history: ShowBackNavigationHistory) { activity?.let { context -> NavigationHistorySheet( @@ -3176,27 +3155,6 @@ class BrowserTabFragment : } } - private fun launchHideTipsDialog( - context: Context, - cta: Cta, - ) { - TextAlertDialogBuilder(context) - .setTitle(R.string.hideTipsTitle) - .setMessage(getString(R.string.hideTipsText)) - .setPositiveButton(R.string.hideTipsButton) - .setNegativeButton(android.R.string.no) - .addEventListener( - object : TextAlertDialogBuilder.EventListener() { - override fun onPositiveButtonClicked() { - launch { - ctaViewModel.hideTipsForever(cta) - } - } - }, - ) - .show() - } - fun omnibarViews(): List = listOf(omnibar.clearTextButton, omnibar.omnibarTextInput, omnibar.searchIcon) override fun onAnimationFinished() { @@ -3661,7 +3619,6 @@ class BrowserTabFragment : activity?.let { activity -> animatorHelper.startTrackersAnimation( context = activity, - shouldRunPartialAnimation = lastSeenCtaViewState?.cta is DaxTrackersBlockedCta, shieldAnimationView = omnibar.shieldIcon, trackersAnimationView = omnibar.trackersAnimation, omnibarViews = omnibarViews(), @@ -3874,99 +3831,48 @@ class BrowserTabFragment : ) { when (configuration) { is HomePanelCta -> showHomeCta(configuration, favorites) - is DaxBubbleCta -> showDaxCta(configuration) - is ExperimentDaxBubbleOptionsCta -> showDaxExperimentCta(configuration) + is DaxBubbleCta -> showDaxOnboardingBubbleCta(configuration) is BubbleCta -> showBubbleCta(configuration) - is DialogCta -> showDaxDialogCta(configuration) - is ExperimentOnboardingDaxDialogCta -> showExperimentDialogCta(configuration) + is OnboardingDaxDialogCta -> showOnboardingDialogCta(configuration) } newBrowserTab.messageCta.gone() } - fun recreateDaxDialogCta() { - val configuration = lastSeenCtaViewState?.cta - if (configuration is DaxDialogCta) { - activity?.let { activity -> - configuration.createCta(activity, daxListener).apply { - showDialogHidingPrevious(this, DAX_DIALOG_DIALOG_TAG) - } - } - } - } - - private fun showDaxDialogCta(configuration: DialogCta) { - hideHomeCta() - hideDaxCta() - activity?.let { activity -> - configuration.createCta(activity, daxListener).apply { - showDialogIfNotExist(this, DAX_DIALOG_DIALOG_TAG) - } - viewModel.onCtaShown() - } - } - - private val daxListener = object : DaxDialogListener { - override fun onDaxDialogDismiss() { - viewModel.onDaxDialogDismissed() - } - - override fun onDaxDialogHideClick() { - viewModel.onUserHideDaxDialog() - } - - override fun onDaxDialogPrimaryCtaClick() { - viewModel.onUserClickCtaOkButton() - } - - override fun onDaxDialogSecondaryCtaClick() { - viewModel.onUserClickCtaSecondaryButton() - } - } - - private fun showDaxCta(configuration: DaxBubbleCta) { - hideHomeBackground() - hideHomeCta() - configuration.showCta(daxDialogCta.daxCtaContainer) - newBrowserTab.newTabLayout.setOnClickListener { daxDialogCta.dialogTextCta.finishAnimation() } - - viewModel.onCtaShown() - } - - private fun showDaxExperimentCta(configuration: ExperimentDaxBubbleOptionsCta) { + private fun showDaxOnboardingBubbleCta(configuration: DaxBubbleCta) { hideHomeBackground() hideHomeCta() configuration.apply { - showCta(daxDialogIntroExperimentCta.daxCtaContainer) + showCta(daxDialogIntroBubbleCta.daxCtaContainer) setOnOptionClicked { userEnteredQuery(it.link) pixel.fire(it.pixel) } } - newBrowserTab.newTabLayout.setOnClickListener { daxDialogIntroExperimentCta.dialogTextCta.finishAnimation() } + newBrowserTab.newTabLayout.setOnClickListener { daxDialogIntroBubbleCta.dialogTextCta.finishAnimation() } viewModel.onCtaShown() } @SuppressLint("ClickableViewAccessibility") - private fun showExperimentDialogCta(configuration: ExperimentOnboardingDaxDialogCta) { + private fun showOnboardingDialogCta(configuration: OnboardingDaxDialogCta) { hideHomeBackground() hideHomeCta() - val onTypingAnimationFinished = if (configuration is ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta) { - { viewModel.onExperimentDaxTypingAnimationFinished() } + val onTypingAnimationFinished = if (configuration is OnboardingDaxDialogCta.DaxTrackersBlockedCta) { + { viewModel.onOnboardingDaxTypingAnimationFinished() } } else { {} } configuration.showOnboardingCta(binding, { viewModel.onUserClickCtaOkButton() }, onTypingAnimationFinished) - if (configuration is ExperimentOnboardingDaxDialogCta.DaxSiteSuggestionsCta) { + if (configuration is OnboardingDaxDialogCta.DaxSiteSuggestionsCta) { configuration.setOnOptionClicked( - daxDialogExperimentOnboardingCta, + daxDialogOnboardingCta, ) { userEnteredQuery(it.link) pixel.fire(it.pixel) viewModel.onUserClickCtaOkButton() } } - binding.webViewContainer.setOnClickListener { daxDialogIntroExperimentCta.dialogTextCta.finishAnimation() } + binding.webViewContainer.setOnClickListener { daxDialogIntroBubbleCta.dialogTextCta.finishAnimation() } viewModel.onCtaShown() } @@ -4021,6 +3927,8 @@ class BrowserTabFragment : private fun hideDaxCta() { daxDialogCta.dialogTextCta.cancelAnimation() daxDialogCta.daxCtaContainer.hide() + daxDialogOnboardingCta.dialogTextCta.cancelAnimation() + daxDialogOnboardingCta.daxCtaContainer.gone() } private fun hideHomeCta() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 637585c274e0..5fd6541deefe 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -103,8 +103,7 @@ import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.browser.webview.SslWarningLayout.Action import com.duckduckgo.app.cta.ui.* -import com.duckduckgo.app.cta.ui.ExperimentDaxBubbleOptionsCta.DaxDialogIntroOption -import com.duckduckgo.app.cta.ui.ExperimentOnboardingDaxDialogCta +import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository @@ -121,8 +120,8 @@ import com.duckduckgo.app.global.model.domainMatchesUrl import com.duckduckgo.app.location.GeoLocationPermissions import com.duckduckgo.app.location.data.LocationPermissionType import com.duckduckgo.app.location.data.LocationPermissionsRepository -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager -import com.duckduckgo.app.onboarding.ui.page.experiment.OnboardingExperimentPixel +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao @@ -254,7 +253,7 @@ class BrowserTabViewModel @Inject constructor( private val privacyProtectionsToggleUsageListener: PrivacyProtectionsToggleUsageListener, private val privacyProtectionsPopupExperimentExternalPixels: PrivacyProtectionsPopupExperimentExternalPixels, private val faviconsFetchingPrompt: FaviconsFetchingPrompt, - private val extendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager, + private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles, private val subscriptions: Subscriptions, private val sslCertificatesFeature: SSLCertificatesFeature, private val bypassedSSLCertificatesRepository: BypassedSSLCertificatesRepository, @@ -719,21 +718,21 @@ class BrowserTabViewModel @Inject constructor( return } - if (currentCtaViewState().cta is ExperimentOnboardingDaxDialogCta) { - onDismissExperimentDaxDialog(currentCtaViewState().cta as ExperimentOnboardingDaxDialogCta) + if (currentCtaViewState().cta is OnboardingDaxDialogCta) { + onDismissOnboardingDaxDialog(currentCtaViewState().cta as OnboardingDaxDialogCta) } when (currentCtaViewState().cta) { - is ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroSearchOptionsCta -> { - if (!DaxDialogIntroOption.getSearchOptions().map { it.link }.contains(query)) { + is DaxBubbleCta.DaxIntroSearchOptionsCta -> { + if (!ctaViewModel.isSuggestedSearchOption(query)) { pixel.fire(OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_CUSTOM) } } - is ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroVisitSiteOptionsCta, - is ExperimentOnboardingDaxDialogCta.DaxSiteSuggestionsCta, + is DaxBubbleCta.DaxIntroVisitSiteOptionsCta, + is OnboardingDaxDialogCta.DaxSiteSuggestionsCta, -> { - if (!DaxDialogIntroOption.getSitesOptions().map { it.link }.contains(query)) { + if (!ctaViewModel.isSuggestedSiteOption(query)) { pixel.fire(OnboardingExperimentPixel.PixelName.ONBOARDING_VISIT_SITE_CUSTOM) } } @@ -2448,8 +2447,7 @@ class BrowserTabViewModel @Inject constructor( } private fun showOrHideKeyboard(cta: Cta?) { - command.value = - if (cta is DialogCta || cta is HomePanelCta) HideKeyboard else ShowKeyboard + command.value = if (cta is HomePanelCta) HideKeyboard else ShowKeyboard } fun registerDaxBubbleCtaDismissed() { @@ -2466,7 +2464,7 @@ class BrowserTabViewModel @Inject constructor( command.value = when (cta) { is HomePanelCta.Survey -> LaunchSurvey(cta.survey) is HomePanelCta.AddWidgetAuto, is HomePanelCta.AddWidgetInstructions -> LaunchAddWidget - is ExperimentOnboardingDaxDialogCta -> onExperimentCtaOkButtonClicked(cta) + is OnboardingDaxDialogCta -> onOnboardingCtaOkButtonClicked(cta) else -> return } } @@ -2520,19 +2518,6 @@ class BrowserTabViewModel @Inject constructor( } } - fun onUserHideDaxDialog() { - val cta = currentCtaViewState().cta ?: return - command.value = DaxCommand.HideDaxDialog(cta) - } - - fun onDaxDialogDismissed() { - val cta = currentCtaViewState().cta ?: return - if (cta is DaxDialogCta.DaxTrackersBlockedCta) { - command.value = DaxCommand.FinishPartialTrackerAnimation - } - onUserDismissedCta() - } - fun onUserDismissedCta() { val cta = currentCtaViewState().cta ?: return viewModelScope.launch { @@ -3170,70 +3155,62 @@ class BrowserTabViewModel @Inject constructor( } } - fun configureBrowserBackground() { - val backgroundRes: Int = - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) R.drawable.onboarding_experiment_background_bitmap else 0 - viewModelScope.launch { - command.value = SetBrowserBackground(backgroundRes) - } - } - - private fun onExperimentCtaOkButtonClicked(experimentCta: ExperimentOnboardingDaxDialogCta): Command? { + private fun onOnboardingCtaOkButtonClicked(onboardingCta: OnboardingDaxDialogCta): Command? { viewModelScope.launch { - ctaViewModel.onUserDismissedCta(experimentCta) + ctaViewModel.onUserDismissedCta(onboardingCta) } - return when (experimentCta) { - is ExperimentOnboardingDaxDialogCta.DaxSerpCta -> { + return when (onboardingCta) { + is OnboardingDaxDialogCta.DaxSerpCta -> { viewModelScope.launch { - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - val cta = withContext(dispatchers.io()) { ctaViewModel.getExperimentSiteSuggestionsDialogCta() } + if (extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled()) { + val cta = withContext(dispatchers.io()) { ctaViewModel.getSiteSuggestionsDialogCta() } ctaViewState.value = currentCtaViewState().copy(cta = cta) if (cta == null) { - command.value = HideExperimentOnboardingDialog(experimentCta) + command.value = HideOnboardingDaxDialog(onboardingCta) } } } null } - is ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta, - is ExperimentOnboardingDaxDialogCta.DaxNoTrackersCta, - is ExperimentOnboardingDaxDialogCta.DaxMainNetworkCta, + is OnboardingDaxDialogCta.DaxTrackersBlockedCta, + is OnboardingDaxDialogCta.DaxNoTrackersCta, + is OnboardingDaxDialogCta.DaxMainNetworkCta, -> { if (currentBrowserViewState().showPrivacyShield.isHighlighted()) { browserViewState.value = currentBrowserViewState().copy(showPrivacyShield = HighlightableButton.Visible(highlighted = false)) } viewModelScope.launch { - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - val cta = withContext(dispatchers.io()) { ctaViewModel.getExperimentFireDialogCta() } + if (extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled()) { + val cta = withContext(dispatchers.io()) { ctaViewModel.getFireDialogCta() } ctaViewState.value = currentCtaViewState().copy(cta = cta) if (cta == null) { - command.value = HideExperimentOnboardingDialog(experimentCta) + command.value = HideOnboardingDaxDialog(onboardingCta) } } } null } - else -> HideExperimentOnboardingDialog(experimentCta) + else -> HideOnboardingDaxDialog(onboardingCta) } } - private fun onDismissExperimentDaxDialog(cta: ExperimentOnboardingDaxDialogCta) { - if (cta is ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta) { + private fun onDismissOnboardingDaxDialog(cta: OnboardingDaxDialogCta) { + if (cta is OnboardingDaxDialogCta.DaxTrackersBlockedCta) { browserViewState.value = currentBrowserViewState().copy(showPrivacyShield = HighlightableButton.Visible(highlighted = false)) } onUserDismissedCta() - command.value = HideExperimentOnboardingDialog(cta) + command.value = HideOnboardingDaxDialog(cta) } fun onFireMenuSelected() { - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { + if (extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled()) { val cta = currentCtaViewState().cta - if (cta is ExperimentOnboardingDaxDialogCta.DaxFireButtonCta) { + if (cta is OnboardingDaxDialogCta.DaxFireButtonCta) { onUserDismissedCta() - command.value = HideExperimentOnboardingDialog(cta) + command.value = HideOnboardingDaxDialog(cta) } if (currentBrowserViewState().fireButton.isHighlighted()) { viewModelScope.launch { @@ -3244,7 +3221,7 @@ class BrowserTabViewModel @Inject constructor( } fun onPrivacyShieldSelected() { - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled() && currentBrowserViewState().showPrivacyShield.isHighlighted()) { + if (extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled() && currentBrowserViewState().showPrivacyShield.isHighlighted()) { browserViewState.value = currentBrowserViewState().copy(showPrivacyShield = HighlightableButton.Visible(highlighted = false)) pixel.fire( pixel = PrivacyDashboardPixels.PRIVACY_DASHBOARD_FIRST_TIME_OPENED, @@ -3257,14 +3234,14 @@ class BrowserTabViewModel @Inject constructor( } } - fun onExperimentDaxTypingAnimationFinished() { + fun onOnboardingDaxTypingAnimationFinished() { browserViewState.value = currentBrowserViewState().copy(showPrivacyShield = HighlightableButton.Visible(highlighted = true)) } override fun onShouldOverride() { val cta = currentCtaViewState().cta - if (cta is ExperimentOnboardingDaxDialogCta) { - onDismissExperimentDaxDialog(cta) + if (cta is OnboardingDaxDialogCta) { + onDismissOnboardingDaxDialog(cta) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index d10843665986..42dba93f4e36 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -34,8 +34,7 @@ import com.duckduckgo.app.browser.history.NavigationHistoryEntry import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState -import com.duckduckgo.app.cta.ui.Cta -import com.duckduckgo.app.cta.ui.ExperimentOnboardingDaxDialogCta +import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.autofill.api.domain.app.LoginCredentials @@ -200,11 +199,6 @@ sealed class Command { class ShowEmailProtectionChooseEmailPrompt(val address: String) : Command() object ShowEmailProtectionInContextSignUpPrompt : Command() - sealed class DaxCommand : Command() { - object FinishPartialTrackerAnimation : DaxCommand() - class HideDaxDialog(val cta: Cta) : DaxCommand() - } - class CancelIncomingAutofillRequest(val url: String) : Command() object LaunchAutofillSettings : Command() class EditWithSelectedQuery(val query: String) : Command() @@ -231,12 +225,11 @@ sealed class Command { data class ScreenLock(val data: JsCallbackData) : Command() object ScreenUnlock : Command() data object ShowFaviconsPrompt : Command() - data class SetBrowserBackground(val backgroundRes: Int) : Command() data class ShowSSLError(val handler: SslErrorHandler, val error: SslErrorResponse) : Command() data object HideSSLError : Command() class LaunchScreen( val screen: String, val payload: String, ) : Command() - data class HideExperimentOnboardingDialog(val experimentCta: ExperimentOnboardingDaxDialogCta) : Command() + data class HideOnboardingDaxDialog(val onboardingCta: OnboardingDaxDialogCta) : Command() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt index 5aa5a42b5000..ecc6a375a677 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt @@ -57,9 +57,6 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( private lateinit var cookieViewBackground: View private var cookieCosmeticHide: Boolean = false - private var runPartialAnimation: Boolean = false - private var completePartialAnimation: Boolean = false - private var enqueueCookiesAnimation = false private var isCookiesAnimationRunning = false private var hasCookiesAnimationBeenCanceled = false @@ -69,16 +66,14 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( override fun startTrackersAnimation( context: Context, - shouldRunPartialAnimation: Boolean, shieldAnimationView: LottieAnimationView, trackersAnimationView: LottieAnimationView, omnibarViews: List, entities: List?, ) { if (isCookiesAnimationRunning) return // If cookies animation is running let it finish to avoid weird glitches with the other animations - if (trackersAnimationView.isAnimating || this.runPartialAnimation) return + if (trackersAnimationView.isAnimating) return - this.runPartialAnimation = shouldRunPartialAnimation this.trackersAnimation = trackersAnimationView this.shieldAnimation = shieldAnimationView @@ -103,17 +98,13 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( this.addAnimatorListener( object : AnimatorListener { override fun onAnimationStart(animation: Animator) { - if (completePartialAnimation) return animateOmnibarOut(omnibarViews).start() } override fun onAnimationEnd(animation: Animator) { - if (!runPartialAnimation) { - animateOmnibarIn(omnibarViews).start() - completePartialAnimation = false - tryToStartCookiesAnimation(context, omnibarViews) - listener?.onAnimationFinished() - } + animateOmnibarIn(omnibarViews).start() + tryToStartCookiesAnimation(context, omnibarViews) + listener?.onAnimationFinished() } override fun onAnimationCancel(animation: Animator) { @@ -124,13 +115,8 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( }, ) - if (runPartialAnimation) { - this.setMaxProgress(0.5f) - shieldAnimationView.setMaxProgress(0.5f) - } else { - this.setMaxProgress(1f) - shieldAnimationView.setMaxProgress(1f) - } + this.setMaxProgress(1f) + shieldAnimationView.setMaxProgress(1f) shieldAnimationView.playAnimation() this.playAnimation() } @@ -149,7 +135,7 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( this.cookieView = cookieAnimationView this.cookieCosmeticHide = cookieCosmeticHide - if (this.trackersAnimation?.isAnimating != true && !runPartialAnimation) { + if (this.trackersAnimation?.isAnimating != true) { startCookiesAnimation(context, omnibarViews) } else { enqueueCookiesAnimation = true @@ -172,19 +158,6 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( omnibarViews.forEach { it.alpha = 1f } } - override fun finishPartialTrackerAnimation() { - val trackersAnimation = this.trackersAnimation ?: return - val shieldAnimation = this.shieldAnimation ?: return - - runPartialAnimation = false - completePartialAnimation = true - - trackersAnimation.setMinAndMaxProgress(0.5f, 1f) - shieldAnimation.setMinAndMaxProgress(0.5f, 1f) - trackersAnimation.playAnimation() - shieldAnimation.playAnimation() - } - private fun tryToStartCookiesAnimation( context: Context, omnibarViews: List, diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt index a85f001f5101..74958925e909 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt @@ -30,7 +30,6 @@ interface BrowserTrackersAnimatorHelper { * Then it plays both animations, [shieldAnimationView] and [trackersAnimationView], at the same time. * When the animations starts, views in [omnibarViews] will fade out. When animation finishes, view in [omnibarViews] will fade in. * - * @param shouldRunPartialAnimation indicates if animation should pause, at 50% of progress, until {@link finishPartialTrackerAnimation()} is called. * @param shieldAnimationView holder of the privacy shield animation. * @param trackersAnimationView holder of the trackers animations. * @param omnibarViews are the views that should be hidden while the animation is running @@ -38,7 +37,6 @@ interface BrowserTrackersAnimatorHelper { */ fun startTrackersAnimation( context: Context, - shouldRunPartialAnimation: Boolean, shieldAnimationView: LottieAnimationView, trackersAnimationView: LottieAnimationView, omnibarViews: List, @@ -80,12 +78,6 @@ interface BrowserTrackersAnimatorHelper { * removes [TrackersAnimatorListener] */ fun removeListener() - - /** - * Finishes a partial tracker animation. - * See startTrackersAnimation.shouldRunPartialAnimation for more details. - */ - fun finishPartialTrackerAnimation() } /** diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 4daf955aa3e8..f19606264a32 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -26,30 +26,33 @@ import android.widget.TextView import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting -import androidx.fragment.app.DialogFragment +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding +import com.duckduckgo.app.browser.databinding.IncludeOnboardingViewDaxDialogBinding import com.duckduckgo.app.cta.model.CtaId +import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption import com.duckduckgo.app.cta.ui.DaxCta.Companion.MAX_DAYS_ALLOWED import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.onboarding.store.OnboardingStore +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelValues.DAX_FIRE_DIALOG_CTA import com.duckduckgo.app.trackerdetection.model.Entity -import com.duckduckgo.common.ui.view.DaxDialogListener import com.duckduckgo.common.ui.view.TypeAnimationTextView -import com.duckduckgo.common.ui.view.TypewriterDaxDialog +import com.duckduckgo.common.ui.view.button.DaxButton import com.duckduckgo.common.ui.view.gone import com.duckduckgo.common.ui.view.hide import com.duckduckgo.common.ui.view.show +import com.duckduckgo.common.ui.view.text.DaxTextView import com.duckduckgo.common.utils.baseHost import com.duckduckgo.common.utils.extensions.html -interface DialogCta { - fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment -} - interface ViewCta { fun showCta(view: View) } @@ -75,23 +78,29 @@ interface Cta { fun pixelOkParameters(): Map } -sealed class DaxDialogCta( +interface OnboardingDaxCta { + fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) + + fun hideOnboardingCta( + binding: FragmentBrowserTabBinding, + ) +} + +sealed class OnboardingDaxDialogCta( override val ctaId: CtaId, + @StringRes open val description: Int?, + @StringRes open val buttonText: Int?, override val shownPixel: Pixel.PixelName?, override val okPixel: Pixel.PixelName?, override val cancelPixel: Pixel.PixelName?, override var ctaPixelParam: String, override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, -) : Cta, DialogCta, DaxCta { - - // This is not an empty CTA. We pass empty values because they actual implementation of DaxDialogCta will take care of them - override fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment = - TypewriterDaxDialog.newInstance( - daxText = "", - primaryButtonText = "", - hideButtonText = "", - ) +) : Cta, DaxCta, OnboardingDaxCta { override fun pixelCancelParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) @@ -99,11 +108,42 @@ sealed class DaxDialogCta( override fun pixelShownParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to addCtaToHistory(ctaPixelParam)) + override fun hideOnboardingCta(binding: FragmentBrowserTabBinding) { + binding.includeOnboardingDaxDialog.root.gone() + } + + internal fun setOnboardingDialogView( + daxText: String, + buttonText: String?, + binding: FragmentBrowserTabBinding, + onTypingAnimationFinished: () -> Unit = {}, + ) { + val daxDialog = binding.includeOnboardingDaxDialog + + daxDialog.root.show() + daxDialog.dialogTextCta.text = "" + daxDialog.hiddenTextCta.text = daxText.html(binding.root.context) + buttonText?.let { + daxDialog.primaryCta.show() + daxDialog.primaryCta.alpha = MIN_ALPHA + daxDialog.primaryCta.text = buttonText + } ?: daxDialog.primaryCta.gone() + binding.includeOnboardingDaxDialog.onboardingDialogSuggestionsContent.gone() + binding.includeOnboardingDaxDialog.onboardingDialogContent.show() + daxDialog.root.alpha = MAX_ALPHA + daxDialog.dialogTextCta.startTypingAnimation(daxText, true) { + ViewCompat.animate(daxDialog.primaryCta).alpha(MAX_ALPHA).duration = DAX_DIALOG_APPEARANCE_ANIMATION + onTypingAnimationFinished.invoke() + } + } + class DaxSerpCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, - ) : DaxDialogCta( + ) : OnboardingDaxDialogCta( CtaId.DAX_DIALOG_SERP, + R.string.onboardingSerpDaxDialogDescription, + R.string.onboardingSerpDaxDialogButton, AppPixelName.ONBOARDING_DAX_CTA_SHOWN, AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, null, @@ -111,14 +151,18 @@ sealed class DaxDialogCta( onboardingStore, appInstallStore, ) { - override fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment { - val dialog = TypewriterDaxDialog.newInstance( - daxText = context.getString(R.string.daxSerpCtaText), - primaryButtonText = context.getString(R.string.daxDialogPhew), - hideButtonText = context.getString(R.string.daxDialogHideButton), + override fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) { + val context = binding.root.context + setOnboardingDialogView( + daxText = description?.let { context.getString(it) }.orEmpty(), + buttonText = buttonText?.let { context.getString(it) }, + binding = binding, ) - dialog.setDaxDialogListener(daxDialogListener) - return dialog + binding.includeOnboardingDaxDialog.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } } } @@ -126,9 +170,10 @@ sealed class DaxDialogCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, val trackers: List, - val host: String, - ) : DaxDialogCta( + ) : OnboardingDaxDialogCta( CtaId.DAX_DIALOG_TRACKERS_FOUND, + null, + R.string.onboardingTrackersBlockedDaxDialogButton, AppPixelName.ONBOARDING_DAX_CTA_SHOWN, AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, null, @@ -136,21 +181,27 @@ sealed class DaxDialogCta( onboardingStore, appInstallStore, ) { - - override fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment { - val dialog = TypewriterDaxDialog.newInstance( - daxText = getDaxText(context), - primaryButtonText = context.getString(R.string.daxDialogHighFive), - toolbarDimmed = false, - hideButtonText = context.getString(R.string.daxDialogHideButton), + override fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) { + val context = binding.root.context + setOnboardingDialogView( + daxText = getTrackersDescription(context, trackers), + buttonText = buttonText?.let { context.getString(it) }, + binding = binding, + onTypingAnimationFinished = onTypingAnimationFinished, ) - dialog.setDaxDialogListener(daxDialogListener) - return dialog + binding.includeOnboardingDaxDialog.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } } @VisibleForTesting - fun getDaxText(context: Context): String { - val trackers = trackers + fun getTrackersDescription( + context: Context, + trackersEntities: List, + ): String { + val trackers = trackersEntities .map { it.displayName } .distinct() @@ -159,9 +210,9 @@ sealed class DaxDialogCta( val size = trackers.size - trackersFiltered.size val quantityString = if (size == 0) { - context.resources.getQuantityString(R.plurals.daxTrackersBlockedCtaZeroText, trackersFiltered.size) + context.resources.getQuantityString(R.plurals.onboardingTrackersBlockedZeroDialogDescription, trackersFiltered.size) } else { - context.resources.getQuantityString(R.plurals.daxTrackersBlockedCtaText, size, size) + context.resources.getQuantityString(R.plurals.onboardingTrackersBlockedDialogDescription, size, size) } return "$trackersText$quantityString" } @@ -172,8 +223,10 @@ sealed class DaxDialogCta( override val appInstallStore: AppInstallStore, val network: String, private val siteHost: String, - ) : DaxDialogCta( + ) : OnboardingDaxDialogCta( CtaId.DAX_DIALOG_NETWORK, + null, + R.string.daxDialogGotIt, AppPixelName.ONBOARDING_DAX_CTA_SHOWN, AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, null, @@ -182,18 +235,22 @@ sealed class DaxDialogCta( appInstallStore, ) { - override fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment { - val dialog = TypewriterDaxDialog.newInstance( - daxText = getDaxText(context), - primaryButtonText = context.getString(R.string.daxDialogGotIt), - hideButtonText = context.getString(R.string.daxDialogHideButton), + override fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) { + val context = binding.root.context + setOnboardingDialogView( + daxText = getTrackersDescription(context), + buttonText = buttonText?.let { context.getString(it) }, + binding = binding, ) - dialog.setDaxDialogListener(daxDialogListener) - return dialog + binding.includeOnboardingDaxDialog.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } } @VisibleForTesting - fun getDaxText(context: Context): String { + fun getTrackersDescription(context: Context): String { return if (isFromSameNetworkDomain()) { context.resources.getString( R.string.daxMainNetworkCtaText, @@ -214,11 +271,13 @@ sealed class DaxDialogCta( private fun isFromSameNetworkDomain(): Boolean = mainTrackerDomains.any { siteHost.contains(it) } } - class DaxNoSerpCta( + class DaxNoTrackersCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, - ) : DaxDialogCta( + ) : OnboardingDaxDialogCta( CtaId.DAX_DIALOG_OTHER, + R.string.daxNonSerpCtaText, + R.string.daxDialogGotIt, AppPixelName.ONBOARDING_DAX_CTA_SHOWN, AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, null, @@ -226,28 +285,126 @@ sealed class DaxDialogCta( onboardingStore, appInstallStore, ) { - override fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment { - val dialog = TypewriterDaxDialog.newInstance( - daxText = context.getString(R.string.daxNonSerpCtaText), - primaryButtonText = context.getString(R.string.daxDialogGotIt), - hideButtonText = context.getString(R.string.daxDialogHideButton), + override fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) { + val context = binding.root.context + setOnboardingDialogView( + daxText = description?.let { context.getString(it) }.orEmpty(), + buttonText = buttonText?.let { context.getString(it) }, + binding = binding, ) - dialog.setDaxDialogListener(daxDialogListener) - return dialog + binding.includeOnboardingDaxDialog.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } + } + } + + class DaxFireButtonCta( + override val onboardingStore: OnboardingStore, + override val appInstallStore: AppInstallStore, + ) : OnboardingDaxDialogCta( + CtaId.DAX_FIRE_BUTTON, + R.string.onboardingFireButtonDaxDialogDescription, + null, + AppPixelName.ONBOARDING_DAX_CTA_SHOWN, + AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, + null, + DAX_FIRE_DIALOG_CTA, + onboardingStore, + appInstallStore, + ) { + override fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) { + val context = binding.root.context + val daxDialog = binding.includeOnboardingDaxDialog + val daxText = description?.let { context.getString(it) }.orEmpty() + + daxDialog.primaryCta.gone() + daxDialog.dialogTextCta.text = "" + daxDialog.hiddenTextCta.text = daxText.html(binding.root.context) + TransitionManager.beginDelayedTransition(binding.includeOnboardingDaxDialog.cardView, AutoTransition()) + daxDialog.dialogTextCta.startTypingAnimation(daxText, true) + } + } + + class DaxSiteSuggestionsCta( + override val onboardingStore: OnboardingStore, + override val appInstallStore: AppInstallStore, + ) : OnboardingDaxDialogCta( + CtaId.DAX_INTRO_VISIT_SITE, + R.string.onboardingSitesDaxDialogDescription, + null, + AppPixelName.ONBOARDING_DAX_CTA_SHOWN, + AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, + null, + Pixel.PixelValues.DAX_INITIAL_VISIT_SITE_CTA, + onboardingStore, + appInstallStore, + ) { + override fun showOnboardingCta( + binding: FragmentBrowserTabBinding, + onPrimaryCtaClicked: () -> Unit, + onTypingAnimationFinished: () -> Unit, + ) { + val context = binding.root.context + val daxDialog = binding.includeOnboardingDaxDialog + val daxText = description?.let { context.getString(it) }.orEmpty() + + binding.includeOnboardingDaxDialog.onboardingDialogContent.gone() + binding.includeOnboardingDaxDialog.onboardingDialogSuggestionsContent.show() + daxDialog.suggestionsDialogTextCta.text = "" + daxDialog.suggestionsHiddenTextCta.text = daxText.html(context) + TransitionManager.beginDelayedTransition(binding.includeOnboardingDaxDialog.cardView, AutoTransition()) + daxDialog.suggestionsDialogTextCta.startTypingAnimation(daxText, true) { + val optionsViews = listOf( + daxDialog.daxDialogOption1, + daxDialog.daxDialogOption2, + daxDialog.daxDialogOption3, + daxDialog.daxDialogOption4, + ) + + optionsViews.forEachIndexed { index, buttonView -> + val options = onboardingStore.getSitesOptions() + options[index].setOptionView(buttonView) + ViewCompat.animate(buttonView).alpha(MAX_ALPHA).duration = DAX_DIALOG_APPEARANCE_ANIMATION + } + } + } + + fun setOnOptionClicked( + daxDialog: IncludeOnboardingViewDaxDialogBinding, + onOptionClicked: (DaxDialogIntroOption) -> Unit, + ) { + val options = onboardingStore.getSitesOptions() + daxDialog.daxDialogOption1.setOnClickListener { onOptionClicked.invoke(options[0]) } + daxDialog.daxDialogOption2.setOnClickListener { onOptionClicked.invoke(options[1]) } + daxDialog.daxDialogOption3.setOnClickListener { onOptionClicked.invoke(options[2]) } + daxDialog.daxDialogOption4.setOnClickListener { onOptionClicked.invoke(options[3]) } } } companion object { - private const val MAX_TRACKERS_SHOWS = 2 + const val SERP = "duckduckgo" - private val mainTrackerDomains = listOf("facebook", "google") val mainTrackerNetworks = listOf("Facebook", "Google") + + private const val MAX_TRACKERS_SHOWS = 2 + private val mainTrackerDomains = listOf("facebook", "google") + private const val DAX_DIALOG_APPEARANCE_ANIMATION = 400L + private const val MAX_ALPHA = 1.0f + private const val MIN_ALPHA = 0.0f } } sealed class DaxBubbleCta( override val ctaId: CtaId, + @StringRes open val title: Int, @StringRes open val description: Int, + open val options: List?, override val shownPixel: Pixel.PixelName?, override val okPixel: Pixel.PixelName?, override val cancelPixel: Pixel.PixelName?, @@ -256,13 +413,55 @@ sealed class DaxBubbleCta( override val appInstallStore: AppInstallStore, ) : Cta, ViewCta, DaxCta { + private var ctaView: View? = null + override fun showCta(view: View) { + ctaView = view + val daxTitle = view.context.getString(title) val daxText = view.context.getString(description) + + if (options.isNullOrEmpty()) { + view.findViewById(R.id.daxDialogOption1).gone() + view.findViewById(R.id.daxDialogOption2).gone() + view.findViewById(R.id.daxDialogOption3).gone() + view.findViewById(R.id.daxDialogOption4).gone() + } else { + options?.let { + val optionsViews = listOf( + view.findViewById(R.id.daxDialogOption1), + view.findViewById(R.id.daxDialogOption2), + view.findViewById(R.id.daxDialogOption3), + view.findViewById(R.id.daxDialogOption4), + ) + optionsViews.forEachIndexed { index, buttonView -> + it[index].setOptionView(buttonView) + ViewCompat.animate(buttonView).alpha(1f).setDuration(500L).startDelay = 2800L + } + } + } view.show() - view.alpha = 1f - view.findViewById(R.id.hiddenTextCta).text = daxText.html(view.context) - view.findViewById(R.id.primaryCta).hide() - view.findViewById(R.id.dialogTextCta).startTypingAnimation(daxText, true) + view.findViewById(R.id.dialogTextCta).text = "" + view.findViewById(R.id.hiddenTextCta).text = daxText.html(view.context) + view.findViewById(R.id.daxBubbleDialogTitle).apply { + alpha = 0f + text = daxTitle.html(view.context) + } + ViewCompat.animate(view).alpha(1f).setDuration(500).setStartDelay(600) + .withEndAction { + ViewCompat.animate(view.findViewById(R.id.daxBubbleDialogTitle)).alpha(1f).setDuration(500) + .withEndAction { + view.findViewById(R.id.dialogTextCta).startTypingAnimation(daxText, true) + } + } + } + + fun setOnOptionClicked(onOptionClicked: (DaxDialogIntroOption) -> Unit) { + options?.let { options -> + ctaView?.findViewById(R.id.daxDialogOption1)?.setOnClickListener { onOptionClicked.invoke(options[0]) } + ctaView?.findViewById(R.id.daxDialogOption2)?.setOnClickListener { onOptionClicked.invoke(options[1]) } + ctaView?.findViewById(R.id.daxDialogOption3)?.setOnClickListener { onOptionClicked.invoke(options[2]) } + ctaView?.findViewById(R.id.daxDialogOption4)?.setOnClickListener { onOptionClicked.invoke(options[3]) } + } } override fun pixelCancelParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) @@ -271,12 +470,14 @@ sealed class DaxBubbleCta( override fun pixelShownParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to addCtaToHistory(ctaPixelParam)) - class DaxIntroCta( + data class DaxIntroSearchOptionsCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, ) : DaxBubbleCta( CtaId.DAX_INTRO, - R.string.daxIntroCtaText, + R.string.onboardingSearchDaxDialogTitle, + R.string.onboardingSearchDaxDialogDescription, + onboardingStore.getSearchOptions(), AppPixelName.ONBOARDING_DAX_CTA_SHOWN, AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, null, @@ -285,12 +486,30 @@ sealed class DaxBubbleCta( appInstallStore, ) - class DaxEndCta( + data class DaxIntroVisitSiteOptionsCta( + override val onboardingStore: OnboardingStore, + override val appInstallStore: AppInstallStore, + ) : DaxBubbleCta( + CtaId.DAX_INTRO_VISIT_SITE, + R.string.onboardingSitesDaxDialogTitle, + R.string.onboardingSitesDaxDialogDescription, + onboardingStore.getSitesOptions(), + AppPixelName.ONBOARDING_DAX_CTA_SHOWN, + AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, + null, + Pixel.PixelValues.DAX_INITIAL_VISIT_SITE_CTA, + onboardingStore, + appInstallStore, + ) + + data class DaxEndCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, ) : DaxBubbleCta( CtaId.DAX_END, - R.string.daxEndCtaText, + R.string.onboardingEndDaxDialogTitle, + R.string.onboardingEndDaxDialogDescription, + null, AppPixelName.ONBOARDING_DAX_CTA_SHOWN, AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, null, @@ -298,6 +517,20 @@ sealed class DaxBubbleCta( onboardingStore, appInstallStore, ) + + data class DaxDialogIntroOption( + val optionText: String, + @DrawableRes val iconRes: Int, + val link: String, + val pixel: PixelName, + ) { + fun setOptionView(buttonView: DaxButton) { + buttonView.apply { + text = optionText + icon = ContextCompat.getDrawable(this.context, iconRes) + } + } + } } sealed class BubbleCta( @@ -345,47 +578,6 @@ sealed class BubbleCta( } } -sealed class DaxFireDialogCta( - override val ctaId: CtaId, - @StringRes open val description: Int, - override val shownPixel: Pixel.PixelName?, - override val okPixel: Pixel.PixelName?, - override val cancelPixel: Pixel.PixelName?, - override var ctaPixelParam: String, - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, -) : Cta, ViewCta, DaxCta { - - override fun showCta(view: View) { - val daxText = view.context.getString(description) - view.show() - view.alpha = 1f - view.findViewById(R.id.hiddenTextCta).text = daxText.html(view.context) - view.findViewById(R.id.primaryCta).gone() - view.findViewById(R.id.dialogTextCta).startTypingAnimation(daxText, true) - } - - override fun pixelCancelParameters(): Map = emptyMap() - - override fun pixelOkParameters(): Map = emptyMap() - - override fun pixelShownParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to addCtaToHistory(ctaPixelParam)) - - class TryClearDataCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : DaxFireDialogCta( - ctaId = CtaId.DAX_FIRE_BUTTON, - description = R.string.daxClearDataCtaText, - shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - okPixel = null, - cancelPixel = null, - ctaPixelParam = DAX_FIRE_DIALOG_CTA, - onboardingStore = onboardingStore, - appInstallStore = appInstallStore, - ) -} - sealed class HomePanelCta( override val ctaId: CtaId, @DrawableRes open val image: Int, diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 2709fa42b48c..53e71d1c4d58 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -30,8 +30,7 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities import com.duckduckgo.app.onboarding.store.* -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager -import com.duckduckgo.app.pixels.AppPixelName +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel @@ -64,7 +63,7 @@ class CtaViewModel @Inject constructor( private val dispatchers: DispatcherProvider, private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, private val surveyRepository: SurveyRepository, - private val extendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager, + private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles, ) { val surveyLiveData: LiveData = surveyRepository.getScheduledLiveSurvey() @@ -109,12 +108,6 @@ class CtaViewModel @Inject constructor( return wasCleared } - suspend fun hideTipsForever(cta: Cta) { - settingsDataStore.hideTips = true - pixel.fire(AppPixelName.ONBOARDING_DAX_ALL_CTA_HIDDEN, cta.pixelCancelParameters()) - userStageStore.stageCompleted(AppStage.DAX_ONBOARDING) - } - fun onCtaShown(cta: Cta) { cta.shownPixel?.let { val canSendPixel = when (cta) { @@ -129,7 +122,7 @@ class CtaViewModel @Inject constructor( suspend fun registerDaxBubbleCtaDismissed(cta: Cta) { withContext(dispatchers.io()) { - if (cta is DaxBubbleCta || cta is ExperimentDaxBubbleOptionsCta) { + if (cta is DaxBubbleCta) { dismissedCtaDao.insert(DismissedCta(cta.ctaId)) completeStageIfDaxOnboardingCompleted() } @@ -190,51 +183,33 @@ class CtaViewModel @Inject constructor( } } - suspend fun getFireDialogCta(): DaxFireDialogCta? { - if (!daxOnboardingActive()) return null - - return withContext(dispatchers.io()) { - if (hideTips() || daxDialogFireEducationShown() || extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - return@withContext null - } - return@withContext DaxFireDialogCta.TryClearDataCta(onboardingStore, appInstallStore) - } - } - - suspend fun getExperimentFireDialogCta(): ExperimentOnboardingDaxDialogCta.DaxFireButtonCta? { + suspend fun getFireDialogCta(): OnboardingDaxDialogCta.DaxFireButtonCta? { if (!daxOnboardingActive() || daxDialogFireEducationShown()) return null return withContext(dispatchers.io()) { - return@withContext ExperimentOnboardingDaxDialogCta.DaxFireButtonCta(onboardingStore, appInstallStore) + return@withContext OnboardingDaxDialogCta.DaxFireButtonCta(onboardingStore, appInstallStore) } } - suspend fun getExperimentSiteSuggestionsDialogCta(): ExperimentOnboardingDaxDialogCta.DaxSiteSuggestionsCta? { + suspend fun getSiteSuggestionsDialogCta(): OnboardingDaxDialogCta.DaxSiteSuggestionsCta? { if (!daxOnboardingActive() || !canShowDaxIntroVisitSiteCta()) return null return withContext(dispatchers.io()) { - return@withContext ExperimentOnboardingDaxDialogCta.DaxSiteSuggestionsCta(onboardingStore, appInstallStore) + return@withContext OnboardingDaxDialogCta.DaxSiteSuggestionsCta(onboardingStore, appInstallStore) } } private suspend fun getHomeCta(): Cta? { + val onboardingEnabled = extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled() return when { - canShowDaxIntroCta() -> { - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroSearchOptionsCta(onboardingStore, appInstallStore) - } else { - DaxBubbleCta.DaxIntroCta(onboardingStore, appInstallStore) - } + canShowDaxIntroCta() && onboardingEnabled -> { + DaxBubbleCta.DaxIntroSearchOptionsCta(onboardingStore, appInstallStore) } - canShowDaxIntroVisitSiteCta() && extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled() -> { - ExperimentDaxBubbleOptionsCta.ExperimentDaxIntroVisitSiteOptionsCta(onboardingStore, appInstallStore) + canShowDaxIntroVisitSiteCta() && onboardingEnabled -> { + DaxBubbleCta.DaxIntroVisitSiteOptionsCta(onboardingStore, appInstallStore) } - canShowDaxCtaEndOfJourney() -> { - if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - ExperimentDaxBubbleOptionsCta.ExperimentDaxEndCta(onboardingStore, appInstallStore) - } else { - DaxBubbleCta.DaxEndCta(onboardingStore, appInstallStore) - } + canShowDaxCtaEndOfJourney() && onboardingEnabled -> { + DaxBubbleCta.DaxEndCta(onboardingStore, appInstallStore) } canShowWidgetCta() -> { @@ -297,46 +272,33 @@ class CtaViewModel @Inject constructor( if (!canShowDaxDialogCta()) return null - // Trackers blocked - if (!daxDialogTrackersFoundShown() && !isSerpUrl(it.url) && it.orderedTrackerBlockedEntities().isNotEmpty()) { - return if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - ExperimentOnboardingDaxDialogCta.DaxTrackersBlockedCta( + if (extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled()) { + // Trackers blocked + if (!daxDialogTrackersFoundShown() && !isSerpUrl(it.url) && it.orderedTrackerBlockedEntities().isNotEmpty()) { + return OnboardingDaxDialogCta.DaxTrackersBlockedCta( onboardingStore, appInstallStore, it.orderedTrackerBlockedEntities(), ) - } else { - DaxDialogCta.DaxTrackersBlockedCta(onboardingStore, appInstallStore, it.orderedTrackerBlockedEntities(), host) } - } - // Is major network - if (it.entity != null) { - it.entity?.let { entity -> - if (!daxDialogNetworkShown() && DaxDialogCta.mainTrackerNetworks.contains(entity.displayName)) { - return if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - ExperimentOnboardingDaxDialogCta.DaxMainNetworkCta(onboardingStore, appInstallStore, entity.displayName, host) - } else { - DaxDialogCta.DaxMainNetworkCta(onboardingStore, appInstallStore, entity.displayName, host) + // Is major network + if (it.entity != null) { + it.entity?.let { entity -> + if (!daxDialogNetworkShown() && OnboardingDaxDialogCta.mainTrackerNetworks.contains(entity.displayName)) { + return OnboardingDaxDialogCta.DaxMainNetworkCta(onboardingStore, appInstallStore, entity.displayName, host) } } } - } - // SERP - if (isSerpUrl(it.url) && !daxDialogSerpShown()) { - return if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - ExperimentOnboardingDaxDialogCta.DaxSerpCta(onboardingStore, appInstallStore) - } else { - DaxDialogCta.DaxSerpCta(onboardingStore, appInstallStore) + // SERP + if (isSerpUrl(it.url) && !daxDialogSerpShown()) { + return OnboardingDaxDialogCta.DaxSerpCta(onboardingStore, appInstallStore) } - } - if (!isSerpUrl(it.url) && !daxDialogOtherShown() && !daxDialogTrackersFoundShown() && !daxDialogNetworkShown()) { - return if (extendedOnboardingExperimentVariantManager.isAestheticUpdatesEnabled()) { - ExperimentOnboardingDaxDialogCta.DaxNoTrackersCta(onboardingStore, appInstallStore) - } else { - DaxDialogCta.DaxNoSerpCta(onboardingStore, appInstallStore) + // No trackers blocked + if (!isSerpUrl(it.url) && !daxDialogOtherShown() && !daxDialogTrackersFoundShown() && !daxDialogNetworkShown()) { + return OnboardingDaxDialogCta.DaxNoTrackersCta(onboardingStore, appInstallStore) } } return null @@ -345,8 +307,6 @@ class CtaViewModel @Inject constructor( private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) - private fun daxDialogIntroVisitSiteShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO_VISIT_SITE) - private fun daxDialogEndShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_END) private fun daxDialogSerpShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP) @@ -361,7 +321,7 @@ class CtaViewModel @Inject constructor( private fun pulseFireButtonShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_FIRE_BUTTON_PULSE) - private fun isSerpUrl(url: String): Boolean = url.contains(DaxDialogCta.SERP) + private fun isSerpUrl(url: String): Boolean = url.contains(OnboardingDaxDialogCta.SERP) private suspend fun daxOnboardingActive(): Boolean = userStageStore.daxOnboardingActive() @@ -411,8 +371,13 @@ class CtaViewModel @Inject constructor( } } + @Deprecated("New users won't have this option available since extended onboarding") private fun hideTips() = settingsDataStore.hideTips + fun isSuggestedSearchOption(query: String): Boolean = onboardingStore.getSearchOptions().map { it.link }.contains(query) + + fun isSuggestedSiteOption(query: String): Boolean = onboardingStore.getSitesOptions().map { it.link }.contains(query) + companion object { private const val MAX_TABS_OPEN_FIRE_EDUCATION = 2 } diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/ExperimentCta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/ExperimentCta.kt deleted file mode 100644 index 5873f8e508c6..000000000000 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/ExperimentCta.kt +++ /dev/null @@ -1,555 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.cta.ui - -import android.content.Context -import android.net.Uri -import android.view.View -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat -import androidx.core.view.ViewCompat -import androidx.transition.AutoTransition -import androidx.transition.TransitionManager -import com.duckduckgo.app.browser.R -import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding -import com.duckduckgo.app.browser.databinding.IncludeExperimentViewDaxDialogBinding -import com.duckduckgo.app.cta.model.CtaId -import com.duckduckgo.app.cta.ui.ExperimentDaxBubbleOptionsCta.DaxDialogIntroOption -import com.duckduckgo.app.global.install.AppInstallStore -import com.duckduckgo.app.onboarding.store.OnboardingStore -import com.duckduckgo.app.onboarding.ui.page.experiment.OnboardingExperimentPixel.PixelName -import com.duckduckgo.app.pixels.AppPixelName -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.trackerdetection.model.Entity -import com.duckduckgo.common.ui.view.TypeAnimationTextView -import com.duckduckgo.common.ui.view.button.DaxButton -import com.duckduckgo.common.ui.view.gone -import com.duckduckgo.common.ui.view.show -import com.duckduckgo.common.ui.view.text.DaxTextView -import com.duckduckgo.common.utils.baseHost -import com.duckduckgo.common.utils.extensions.html -import com.duckduckgo.mobile.android.R as commonR - -sealed class ExperimentDaxBubbleOptionsCta( - override val ctaId: CtaId, - @StringRes open val title: Int, - @StringRes open val description: Int, - open val options: List?, - override val shownPixel: Pixel.PixelName?, - override val okPixel: Pixel.PixelName?, - override val cancelPixel: Pixel.PixelName?, - override var ctaPixelParam: String, - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, -) : Cta, ViewCta, DaxCta { - - private var ctaView: View? = null - - override fun showCta(view: View) { - ctaView = view - val daxTitle = view.context.getString(title) - val daxText = view.context.getString(description) - - if (options.isNullOrEmpty()) { - view.findViewById(R.id.daxDialogOption1).gone() - view.findViewById(R.id.daxDialogOption2).gone() - view.findViewById(R.id.daxDialogOption3).gone() - view.findViewById(R.id.daxDialogOption4).gone() - } else { - options?.let { - val optionsViews = listOf( - view.findViewById(R.id.daxDialogOption1), - view.findViewById(R.id.daxDialogOption2), - view.findViewById(R.id.daxDialogOption3), - view.findViewById(R.id.daxDialogOption4), - ) - optionsViews.forEachIndexed { index, buttonView -> - it[index].setOptionView(buttonView) - ViewCompat.animate(buttonView).alpha(1f).setDuration(500L).startDelay = 2800L - } - } - } - view.show() - view.findViewById(R.id.dialogTextCta).text = "" - view.findViewById(R.id.hiddenTextCta).text = daxText.html(view.context) - view.findViewById(R.id.experimentDialogTitle).apply { - alpha = 0f - text = daxTitle.html(view.context) - } - ViewCompat.animate(view).alpha(1f).setDuration(500).setStartDelay(600) - .withEndAction { - ViewCompat.animate(view.findViewById(R.id.experimentDialogTitle)).alpha(1f).setDuration(500) - .withEndAction { - view.findViewById(R.id.dialogTextCta).startTypingAnimation(daxText, true) - } - } - } - - fun setOnOptionClicked(onOptionClicked: (DaxDialogIntroOption) -> Unit) { - options?.let { options -> - ctaView?.findViewById(R.id.daxDialogOption1)?.setOnClickListener { onOptionClicked.invoke(options[0]) } - ctaView?.findViewById(R.id.daxDialogOption2)?.setOnClickListener { onOptionClicked.invoke(options[1]) } - ctaView?.findViewById(R.id.daxDialogOption3)?.setOnClickListener { onOptionClicked.invoke(options[2]) } - ctaView?.findViewById(R.id.daxDialogOption4)?.setOnClickListener { onOptionClicked.invoke(options[3]) } - } - } - - override fun pixelCancelParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) - - override fun pixelOkParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) - - override fun pixelShownParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to addCtaToHistory(ctaPixelParam)) - - data class ExperimentDaxIntroSearchOptionsCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentDaxBubbleOptionsCta( - CtaId.DAX_INTRO, - R.string.onboardingSearchDaxDialogTitle, - R.string.onboardingSearchDaxDialogDescription, - DaxDialogIntroOption.getSearchOptions(), - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_INITIAL_CTA, - onboardingStore, - appInstallStore, - ) - - data class ExperimentDaxIntroVisitSiteOptionsCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentDaxBubbleOptionsCta( - CtaId.DAX_INTRO_VISIT_SITE, - R.string.onboardingSitesDaxDialogTitle, - R.string.onboardingSitesDaxDialogDescription, - DaxDialogIntroOption.getSitesOptions(), - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_INITIAL_VISIT_SITE_CTA, - onboardingStore, - appInstallStore, - ) - - data class ExperimentDaxEndCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentDaxBubbleOptionsCta( - CtaId.DAX_END, - R.string.onboardingEndDaxDialogTitle, - R.string.onboardingEndDaxDialogDescription, - null, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_END_CTA, - onboardingStore, - appInstallStore, - ) - - data class DaxDialogIntroOption( - @StringRes val textRes: Int, - @DrawableRes val iconRes: Int, - val link: String, - val pixel: PixelName, - ) { - fun setOptionView(buttonView: DaxButton) { - buttonView.apply { - text = this.context.getString(textRes) - icon = ContextCompat.getDrawable(this.context, iconRes) - } - } - - companion object { - fun getSearchOptions(): List = - listOf( - DaxDialogIntroOption( - R.string.onboardingSearchDaxDialogOption1, - commonR.drawable.ic_find_search_16, - "how to say duck in spanish", - PixelName.ONBOARDING_SEARCH_SAY_DUCK, - ), - DaxDialogIntroOption( - R.string.onboardingSearchDaxDialogOption2, - commonR.drawable.ic_find_search_16, - "mighty ducks cast", - PixelName.ONBOARDING_SEARCH_MIGHTY_DUCK, - ), - DaxDialogIntroOption( - R.string.onboardingSearchDaxDialogOption3, - commonR.drawable.ic_find_search_16, - "local weather", - PixelName.ONBOARDING_SEARCH_WEATHER, - ), - DaxDialogIntroOption( - R.string.onboardingSearchDaxDialogOption4, - commonR.drawable.ic_wand_16, - "chocolate chip cookie recipes", - PixelName.ONBOARDING_SEARCH_SURPRISE_ME, - ), - ) - - fun getSitesOptions(): List = - listOf( - DaxDialogIntroOption( - R.string.onboardingSitesDaxDialogOption1, - commonR.drawable.ic_globe_gray_16dp, - "espn.com", - PixelName.ONBOARDING_VISIT_SITE_ESPN, - ), - DaxDialogIntroOption( - R.string.onboardingSitesDaxDialogOption2, - commonR.drawable.ic_globe_gray_16dp, - "yahoo.com", - PixelName.ONBOARDING_VISIT_SITE_YAHOO, - ), - DaxDialogIntroOption( - R.string.onboardingSitesDaxDialogOption3, - commonR.drawable.ic_globe_gray_16dp, - "ebay.com", - PixelName.ONBOARDING_VISIT_SITE_EBAY, - ), - DaxDialogIntroOption( - R.string.onboardingSitesDaxDialogOption4, - commonR.drawable.ic_wand_16, - "britannica.com/animal/duck", - PixelName.ONBOARDING_VISIT_SITE_SURPRISE_ME, - ), - ) - } - } -} - -interface ExperimentDaxCta { - fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) - - fun hideOnboardingCta( - binding: FragmentBrowserTabBinding, - ) -} - -sealed class ExperimentOnboardingDaxDialogCta( - override val ctaId: CtaId, - @StringRes open val description: Int?, - @StringRes open val buttonText: Int?, - override val shownPixel: Pixel.PixelName?, - override val okPixel: Pixel.PixelName?, - override val cancelPixel: Pixel.PixelName?, - override var ctaPixelParam: String, - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, -) : Cta, ExperimentDaxCta, DaxCta { - - override fun pixelCancelParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) - - override fun pixelOkParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) - - override fun pixelShownParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to addCtaToHistory(ctaPixelParam)) - - override fun hideOnboardingCta(binding: FragmentBrowserTabBinding) { - binding.includeOnboardingDaxDialogExperiment.root.gone() - } - - internal fun setOnboardingDialogView( - daxText: String, - buttonText: String?, - binding: FragmentBrowserTabBinding, - onTypingAnimationFinished: () -> Unit = {}, - ) { - val daxDialog = binding.includeOnboardingDaxDialogExperiment - - daxDialog.root.show() - daxDialog.dialogTextCta.text = "" - daxDialog.hiddenTextCta.text = daxText.html(binding.root.context) - buttonText?.let { - daxDialog.primaryCta.show() - daxDialog.primaryCta.alpha = MIN_ALPHA - daxDialog.primaryCta.text = buttonText - } ?: daxDialog.primaryCta.gone() - binding.includeOnboardingDaxDialogExperiment.onboardingDialogSuggestionsContent.gone() - binding.includeOnboardingDaxDialogExperiment.onboardingDialogContent.show() - daxDialog.root.alpha = MAX_ALPHA - daxDialog.dialogTextCta.startTypingAnimation(daxText, true) { - ViewCompat.animate(daxDialog.primaryCta).alpha(MAX_ALPHA).duration = DAX_DIALOG_APPEARANCE_ANIMATION - onTypingAnimationFinished.invoke() - } - } - - class DaxSerpCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentOnboardingDaxDialogCta( - CtaId.DAX_DIALOG_SERP, - R.string.onboardingSerpDaxDialogDescription, - R.string.onboardingSerpDaxDialogButton, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_SERP_CTA, - onboardingStore, - appInstallStore, - ) { - override fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) { - val context = binding.root.context - setOnboardingDialogView( - daxText = description?.let { context.getString(it) }.orEmpty(), - buttonText = buttonText?.let { context.getString(it) }, - binding = binding, - ) - binding.includeOnboardingDaxDialogExperiment.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } - } - } - - class DaxTrackersBlockedCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - val trackers: List, - ) : ExperimentOnboardingDaxDialogCta( - CtaId.DAX_DIALOG_TRACKERS_FOUND, - null, - R.string.onboardingTrackersBlockedDaxDialogButton, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_TRACKERS_BLOCKED_CTA, - onboardingStore, - appInstallStore, - ) { - override fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) { - val context = binding.root.context - setOnboardingDialogView( - daxText = getTrackersDescription(context, trackers), - buttonText = buttonText?.let { context.getString(it) }, - binding = binding, - onTypingAnimationFinished = onTypingAnimationFinished, - ) - binding.includeOnboardingDaxDialogExperiment.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } - } - - private fun getTrackersDescription( - context: Context, - trackersEntities: List, - ): String { - val trackers = trackersEntities - .map { it.displayName } - .distinct() - - val trackersFiltered = trackers.take(MAX_TRACKERS_SHOWS) - val trackersText = trackersFiltered.joinToString(", ") - val size = trackers.size - trackersFiltered.size - val quantityString = - if (size == 0) { - context.resources.getQuantityString(R.plurals.onboardingTrackersBlockedZeroDialogDescription, trackersFiltered.size) - } else { - context.resources.getQuantityString(R.plurals.onboardingTrackersBlockedDialogDescription, size, size) - } - return "$trackersText$quantityString" - } - } - - class DaxMainNetworkCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - val network: String, - private val siteHost: String, - ) : ExperimentOnboardingDaxDialogCta( - CtaId.DAX_DIALOG_NETWORK, - null, - R.string.daxDialogGotIt, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_NETWORK_CTA_1, - onboardingStore, - appInstallStore, - ) { - - override fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) { - val context = binding.root.context - setOnboardingDialogView( - daxText = getTrackersDescription(context), - buttonText = buttonText?.let { context.getString(it) }, - binding = binding, - ) - binding.includeOnboardingDaxDialogExperiment.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } - } - - @VisibleForTesting - fun getTrackersDescription(context: Context): String { - return if (isFromSameNetworkDomain()) { - context.resources.getString( - R.string.daxMainNetworkCtaText, - network, - Uri.parse(siteHost).baseHost?.removePrefix("m."), - network, - ) - } else { - context.resources.getString( - R.string.daxMainNetworkOwnedCtaText, - network, - Uri.parse(siteHost).baseHost?.removePrefix("m."), - network, - ) - } - } - - private fun isFromSameNetworkDomain(): Boolean = mainTrackerDomains.any { siteHost.contains(it) } - } - - class DaxNoTrackersCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentOnboardingDaxDialogCta( - CtaId.DAX_DIALOG_OTHER, - R.string.daxNonSerpCtaText, - R.string.daxDialogGotIt, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_NO_TRACKERS_CTA, - onboardingStore, - appInstallStore, - ) { - override fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) { - val context = binding.root.context - setOnboardingDialogView( - daxText = description?.let { context.getString(it) }.orEmpty(), - buttonText = buttonText?.let { context.getString(it) }, - binding = binding, - ) - binding.includeOnboardingDaxDialogExperiment.primaryCta.setOnClickListener { onPrimaryCtaClicked.invoke() } - } - } - - class DaxFireButtonCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentOnboardingDaxDialogCta( - CtaId.DAX_FIRE_BUTTON, - R.string.onboardingFireButtonDaxDialogDescription, - null, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_FIRE_DIALOG_CTA, - onboardingStore, - appInstallStore, - ) { - override fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) { - val context = binding.root.context - val daxDialog = binding.includeOnboardingDaxDialogExperiment - val daxText = description?.let { context.getString(it) }.orEmpty() - - daxDialog.primaryCta.gone() - daxDialog.dialogTextCta.text = "" - daxDialog.hiddenTextCta.text = daxText.html(binding.root.context) - TransitionManager.beginDelayedTransition(binding.includeOnboardingDaxDialogExperiment.cardView, AutoTransition()) - daxDialog.dialogTextCta.startTypingAnimation(daxText, true) - } - } - - class DaxSiteSuggestionsCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - ) : ExperimentOnboardingDaxDialogCta( - CtaId.DAX_INTRO_VISIT_SITE, - R.string.onboardingSitesDaxDialogDescription, - null, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_INITIAL_VISIT_SITE_CTA, - onboardingStore, - appInstallStore, - ) { - override fun showOnboardingCta( - binding: FragmentBrowserTabBinding, - onPrimaryCtaClicked: () -> Unit, - onTypingAnimationFinished: () -> Unit, - ) { - val context = binding.root.context - val daxDialog = binding.includeOnboardingDaxDialogExperiment - val daxText = description?.let { context.getString(it) }.orEmpty() - - binding.includeOnboardingDaxDialogExperiment.onboardingDialogContent.gone() - binding.includeOnboardingDaxDialogExperiment.onboardingDialogSuggestionsContent.show() - daxDialog.suggestionsDialogTextCta.text = "" - daxDialog.suggestionsHiddenTextCta.text = daxText.html(context) - TransitionManager.beginDelayedTransition(binding.includeOnboardingDaxDialogExperiment.cardView, AutoTransition()) - daxDialog.suggestionsDialogTextCta.startTypingAnimation(daxText, true) { - val optionsViews = listOf( - daxDialog.daxDialogOption1, - daxDialog.daxDialogOption2, - daxDialog.daxDialogOption3, - daxDialog.daxDialogOption4, - ) - - optionsViews.forEachIndexed { index, buttonView -> - val options = DaxDialogIntroOption.getSitesOptions() - options[index].setOptionView(buttonView) - ViewCompat.animate(buttonView).alpha(MAX_ALPHA).duration = DAX_DIALOG_APPEARANCE_ANIMATION - } - } - } - - fun setOnOptionClicked( - daxDialog: IncludeExperimentViewDaxDialogBinding, - onOptionClicked: (DaxDialogIntroOption) -> Unit, - ) { - val options = DaxDialogIntroOption.getSitesOptions() - daxDialog.daxDialogOption1.setOnClickListener { onOptionClicked.invoke(options[0]) } - daxDialog.daxDialogOption2.setOnClickListener { onOptionClicked.invoke(options[1]) } - daxDialog.daxDialogOption3.setOnClickListener { onOptionClicked.invoke(options[2]) } - daxDialog.daxDialogOption4.setOnClickListener { onOptionClicked.invoke(options[3]) } - } - } - - companion object { - private const val MAX_TRACKERS_SHOWS = 2 - private val mainTrackerDomains = listOf("facebook", "google") - private const val DAX_DIALOG_APPEARANCE_ANIMATION = 400L - private const val MAX_ALPHA = 1.0f - private const val MIN_ALPHA = 0.0f - } -} diff --git a/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt b/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt index f77e2f8eedb9..37e46316c085 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt @@ -24,7 +24,6 @@ import com.duckduckgo.app.browser.rating.di.RatingModule import com.duckduckgo.app.email.di.EmailModule import com.duckduckgo.app.global.DuckDuckGoApplication import com.duckduckgo.app.onboarding.di.OnboardingModule -import com.duckduckgo.app.onboarding.di.WelcomePageModule import com.duckduckgo.app.surrogates.di.ResourceSurrogateModule import com.duckduckgo.app.usage.di.AppUsageModule import com.duckduckgo.di.scopes.AppScope @@ -66,7 +65,6 @@ import retrofit2.Retrofit FileModule::class, CoroutinesModule::class, CertificateTrustedStoreModule::class, - WelcomePageModule::class, FormatterModule::class, EmailModule::class, ], diff --git a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt index ee496688f270..8c503d432702 100644 --- a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt @@ -24,8 +24,8 @@ import com.duckduckgo.app.global.install.AppInstallSharedPreferences import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.onboarding.store.AppUserStageStore -import com.duckduckgo.app.onboarding.store.OnboardingSharedPreferences import com.duckduckgo.app.onboarding.store.OnboardingStore +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.app.statistics.store.StatisticsSharedPreferences @@ -51,7 +51,7 @@ abstract class StoreModule { abstract fun bindThemingStore(themeDataStore: ThemingSharedPreferences): ThemingDataStore @Binds - abstract fun bindOnboardingStore(onboardingStore: OnboardingSharedPreferences): OnboardingStore + abstract fun bindOnboardingStore(onboardingStore: OnboardingStoreImpl): OnboardingStore @Binds abstract fun bindTabRepository(tabRepository: TabDataRepository): TabRepository diff --git a/app/src/main/java/com/duckduckgo/app/global/view/FireDialog.kt b/app/src/main/java/com/duckduckgo/app/global/view/FireDialog.kt index bc6ed0a91b6e..32fd3d06b949 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/FireDialog.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/FireDialog.kt @@ -25,14 +25,8 @@ import android.provider.Settings import android.provider.Settings.Global.ANIMATOR_DURATION_SCALE import android.view.LayoutInflater import androidx.core.content.ContextCompat -import androidx.core.view.doOnDetach -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope import com.airbnb.lottie.RenderMode -import com.duckduckgo.app.browser.databinding.IncludeDaxDialogCtaBinding import com.duckduckgo.app.browser.databinding.SheetFireClearDataBinding -import com.duckduckgo.app.cta.ui.CtaViewModel -import com.duckduckgo.app.cta.ui.DaxFireDialogCta import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.view.FireDialog.FireDialogClearAllEvent.AnimationFinished @@ -58,7 +52,6 @@ private const val ANIMATION_SPEED_INCREMENT = 0.15f @SuppressLint("NoBottomSheetDialog") class FireDialog( context: Context, - private val ctaViewModel: CtaViewModel, private val clearPersonalDataAction: ClearDataAction, private val pixel: Pixel, private val settingsDataStore: SettingsDataStore, @@ -68,11 +61,8 @@ class FireDialog( ) : BottomSheetDialog(context, com.duckduckgo.mobile.android.R.style.Widget_DuckDuckGo_FireDialog) { private lateinit var binding: SheetFireClearDataBinding - private lateinit var fireCtaBinding: IncludeDaxDialogCtaBinding var clearStarted: (() -> Unit) = {} - val ctaVisible: Boolean - get() = if (this::fireCtaBinding.isInitialized) fireCtaBinding.daxCtaContainer.isVisible else false private val accelerateAnimatorUpdateListener = object : ValueAnimator.AnimatorUpdateListener { override fun onAnimationUpdate(animation: ValueAnimator) { @@ -88,20 +78,12 @@ class FireDialog( init { val inflater = LayoutInflater.from(context) binding = SheetFireClearDataBinding.inflate(inflater) - binding.fireCtaViewStub.setOnInflateListener { _, inflated -> - fireCtaBinding = IncludeDaxDialogCtaBinding.bind(inflated) - } setContentView(binding.root) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - lifecycleScope.launch { - ctaViewModel.getFireDialogCta()?.let { - configureFireDialogCta(it) - } - } binding.clearAllOption.setOnClickListener { onClearOptionClicked() } @@ -126,22 +108,8 @@ class FireDialog( binding.fireAnimationView.enableMergePathsForKitKatAndAbove(true) } - private fun configureFireDialogCta(cta: DaxFireDialogCta) { - binding.fireCtaViewStub.inflate() - cta.showCta(fireCtaBinding.daxCtaContainer) - ctaViewModel.onCtaShown(cta) - onClearDataOptionsDismissed = { - appCoroutineScope.launch(dispatcherProvider.io()) { - ctaViewModel.onUserDismissedCta(cta) - } - } - fireCtaBinding.daxCtaContainer.doOnDetach { - onClearDataOptionsDismissed() - } - } - private fun onClearOptionClicked() { - pixel.enqueueFire(if (ctaVisible) FIRE_DIALOG_PROMOTED_CLEAR_PRESSED else FIRE_DIALOG_CLEAR_PRESSED) + pixel.enqueueFire(FIRE_DIALOG_CLEAR_PRESSED) pixel.enqueueFire( pixel = FIRE_DIALOG_ANIMATION, parameters = mapOf(FIRE_ANIMATION to settingsDataStore.selectedFireAnimation.getPixelValue()), diff --git a/app/src/main/java/com/duckduckgo/app/launch/LaunchBridgeActivity.kt b/app/src/main/java/com/duckduckgo/app/launch/LaunchBridgeActivity.kt index d31b776ec9e1..6d621d25b639 100644 --- a/app/src/main/java/com/duckduckgo/app/launch/LaunchBridgeActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/launch/LaunchBridgeActivity.kt @@ -49,16 +49,17 @@ class LaunchBridgeActivity : DuckDuckGoActivity() { private fun processCommand(it: LaunchViewModel.Command) { when (it) { is LaunchViewModel.Command.Onboarding -> { - showOnboarding(it.forceLightTheme) + showOnboarding() } + is LaunchViewModel.Command.Home -> { showHome() } } } - private fun showOnboarding(forceLightTheme: Boolean) { - startActivity(OnboardingActivity.intent(this, forceLightTheme)) + private fun showOnboarding() { + startActivity(OnboardingActivity.intent(this)) finish() } diff --git a/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt index 5f09db926eda..2b95f7680c6f 100644 --- a/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt @@ -21,7 +21,6 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.global.SingleLiveEvent import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.store.isNewUser -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager import com.duckduckgo.app.referral.AppInstallationReferrerStateListener import com.duckduckgo.app.referral.AppInstallationReferrerStateListener.Companion.MAX_REFERRER_WAIT_TIME_MS import com.duckduckgo.di.scopes.ActivityScope @@ -33,14 +32,13 @@ import timber.log.Timber class LaunchViewModel @Inject constructor( private val userStageStore: UserStageStore, private val appReferrerStateListener: AppInstallationReferrerStateListener, - private val extendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager, ) : ViewModel() { val command: SingleLiveEvent = SingleLiveEvent() sealed class Command { - data class Onboarding(val forceLightTheme: Boolean = true) : Command() + data object Onboarding : Command() data class Home(val replaceExistingSearch: Boolean = false) : Command() } @@ -48,8 +46,7 @@ class LaunchViewModel @Inject constructor( waitForReferrerData() if (userStageStore.isNewUser()) { - extendedOnboardingExperimentVariantManager.setExperimentVariants() - command.value = Command.Onboarding(forceLightTheme = !extendedOnboardingExperimentVariantManager.isComparisonChartEnabled()) + command.value = Command.Onboarding } else { command.value = Command.Home() } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/di/OnboardingModule.kt b/app/src/main/java/com/duckduckgo/app/onboarding/di/OnboardingModule.kt index e94eb09d107b..562497875641 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/di/OnboardingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/di/OnboardingModule.kt @@ -18,11 +18,13 @@ package com.duckduckgo.app.onboarding.di import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.global.DefaultRoleBrowserDialog +import com.duckduckgo.app.global.RealDefaultRoleBrowserDialog +import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.onboarding.ui.OnboardingFragmentPageBuilder import com.duckduckgo.app.onboarding.ui.OnboardingPageBuilder import com.duckduckgo.app.onboarding.ui.OnboardingPageManager import com.duckduckgo.app.onboarding.ui.OnboardingPageManagerWithTrackerBlocking -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import dagger.Module import dagger.Provides @@ -36,13 +38,11 @@ class OnboardingModule { defaultRoleBrowserDialog: DefaultRoleBrowserDialog, onboardingPageBuilder: OnboardingPageBuilder, defaultBrowserDetector: DefaultBrowserDetector, - extendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager, ): OnboardingPageManager { return OnboardingPageManagerWithTrackerBlocking( defaultRoleBrowserDialog, onboardingPageBuilder, defaultBrowserDetector, - extendedOnboardingExperimentVariantManager, ) } @@ -51,4 +51,10 @@ class OnboardingModule { fun onboardingPageBuilder(): OnboardingPageBuilder { return OnboardingFragmentPageBuilder() } + + @Provides + fun defaultRoleBrowserDialog( + appInstallStore: AppInstallStore, + appBuildConfig: AppBuildConfig, + ): DefaultRoleBrowserDialog = RealDefaultRoleBrowserDialog(appInstallStore, appBuildConfig) } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/di/WelcomePageModule.kt b/app/src/main/java/com/duckduckgo/app/onboarding/di/WelcomePageModule.kt deleted file mode 100644 index b40c2e24c970..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/di/WelcomePageModule.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2018 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.onboarding.di - -import android.content.Context -import com.duckduckgo.app.global.DefaultRoleBrowserDialog -import com.duckduckgo.app.global.RealDefaultRoleBrowserDialog -import com.duckduckgo.app.global.install.AppInstallStore -import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModelFactory -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.appbuildconfig.api.AppBuildConfig -import dagger.Module -import dagger.Provides - -@Module -class WelcomePageModule { - - @Provides - fun welcomePageViewModelFactory( - appInstallStore: AppInstallStore, - context: Context, - pixel: Pixel, - defaultRoleBrowserDialog: DefaultRoleBrowserDialog, - ) = WelcomePageViewModelFactory(appInstallStore, context, pixel, defaultRoleBrowserDialog) - - @Provides - fun defaultRoleBrowserDialog( - appInstallStore: AppInstallStore, - appBuildConfig: AppBuildConfig, - ): DefaultRoleBrowserDialog = RealDefaultRoleBrowserDialog(appInstallStore, appBuildConfig) -} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingSharedPreferences.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingSharedPreferences.kt deleted file mode 100644 index 418c9ce543ee..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingSharedPreferences.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2018 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.onboarding.store - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import javax.inject.Inject - -class OnboardingSharedPreferences @Inject constructor(private val context: Context) : OnboardingStore { - - override var onboardingDialogJourney: String? - get() = preferences.getString(ONBOARDING_JOURNEY, null) - set(dialogJourney) = preferences.edit { putString(ONBOARDING_JOURNEY, dialogJourney) } - - private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } - - companion object { - const val FILENAME = "com.duckduckgo.app.onboarding.settings" - const val ONBOARDING_JOURNEY = "onboardingJourney" - } -} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt index a47413af77cb..70db74af85d1 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt @@ -16,6 +16,10 @@ package com.duckduckgo.app.onboarding.store +import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption + interface OnboardingStore { var onboardingDialogJourney: String? + fun getSearchOptions(): List + fun getSitesOptions(): List } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt new file mode 100644 index 000000000000..98628f7b3ab2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.app.onboarding.store + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_MIGHTY_DUCK +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_SAY_DUCK +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_SURPRISE_ME +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_SEARCH_WEATHER +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_VISIT_SITE_EBAY +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_VISIT_SITE_ESPN +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_VISIT_SITE_SURPRISE_ME +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.ONBOARDING_VISIT_SITE_YAHOO +import com.duckduckgo.mobile.android.R.drawable +import java.util.Locale +import javax.inject.Inject + +class OnboardingStoreImpl @Inject constructor(private val context: Context) : OnboardingStore { + + private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } + + override var onboardingDialogJourney: String? + get() = preferences.getString(ONBOARDING_JOURNEY, null) + set(dialogJourney) = preferences.edit { putString(ONBOARDING_JOURNEY, dialogJourney) } + + override fun getSearchOptions(): List { + val country = Locale.getDefault().country + val language = Locale.getDefault().language + + return listOf( + DaxDialogIntroOption( + optionText = if (language == "en") { + context.getString(R.string.onboardingSearchDaxDialogOption1English) + } else { + context.getString(R.string.onboardingSearchDaxDialogOption1) + }, + iconRes = drawable.ic_find_search_16, + link = if (language == "en") "how to say duck in spanish" else context.getString(R.string.onboardingSearchQueryOption1), + pixel = ONBOARDING_SEARCH_SAY_DUCK, + ), + DaxDialogIntroOption( + optionText = if (country == "US") "mighty ducks cast" else context.getString(R.string.onboardingSearchDaxDialogOption2), + iconRes = drawable.ic_find_search_16, + link = if (country == "US") "mighty ducks cast" else context.getString(R.string.onboardingSearchDaxDialogOption2), + pixel = ONBOARDING_SEARCH_MIGHTY_DUCK, + ), + DaxDialogIntroOption( + optionText = context.getString(R.string.onboardingSearchDaxDialogOption3), + iconRes = drawable.ic_find_search_16, + link = context.getString(R.string.onboardingSearchDaxDialogOption3), + pixel = ONBOARDING_SEARCH_WEATHER, + ), + DaxDialogIntroOption( + optionText = context.getString(R.string.onboardingSearchDaxDialogOption4), + iconRes = drawable.ic_wand_16, + link = if (country == "US") "chocolate chip cookie recipes" else context.getString(R.string.onboardingSearchQueryOption4), + pixel = ONBOARDING_SEARCH_SURPRISE_ME, + ), + ) + } + + override fun getSitesOptions(): List { + val site1: String + val site2: String + val site3: String + val site4Query: String + when (Locale.getDefault().country) { + "ID" -> { + site1 = "bolasport.com " + site2 = "kompas.com" + site3 = "tokopedia.com" + site4Query = "britannica.com/animal/duck" + } + + "GB" -> { + site1 = "skysports.com " + site2 = "bbc.co.uk" + site3 = "ebay.com" + site4Query = "britannica.com/animal/duck" + } + + "DE" -> { + site1 = "kicker.de" + site2 = "tagesschau.de" + site3 = "ebay.com" + site4Query = "duden.de/rechtschreibung/Ente" + } + + "CA" -> { + site1 = "tsn.ca" + site2 = "cbc.ca" + site3 = "canadiantire.ca" + site4Query = "britannica.com/animal/duck" + } + + "NL" -> { + site1 = "voetbalprimeur.nl" + site2 = "nu.nl" + site3 = "bol.com" + site4Query = "woorden.org/woord/eend" + } + + "AU" -> { + site1 = "afl.com.au" + site2 = "abc.net.au" + site3 = "ebay.com" + site4Query = "britannica.com/animal/duck" + } + + "SE" -> { + site1 = "svenskafans.com" + site2 = "dn.se" + site3 = "tradera.com" + site4Query = "synonymer.se/sv-syn/anka" + } + + "IE" -> { + site1 = "skysports.com" + site2 = "bbc.co.uk " + site3 = "ebay.com" + site4Query = "britannica.com/animal/duck" + } + + else -> { + site1 = "espn.com" + site2 = "yahoo.com" + site3 = "ebay.com" + site4Query = "britannica.com/animal/duck" + } + } + return listOf( + DaxDialogIntroOption( + optionText = site1, + iconRes = drawable.ic_globe_gray_16dp, + link = site1, + pixel = ONBOARDING_VISIT_SITE_ESPN, + ), + DaxDialogIntroOption( + optionText = site2, + iconRes = drawable.ic_globe_gray_16dp, + link = site2, + pixel = ONBOARDING_VISIT_SITE_YAHOO, + ), + DaxDialogIntroOption( + optionText = site3, + iconRes = drawable.ic_globe_gray_16dp, + link = site3, + pixel = ONBOARDING_VISIT_SITE_EBAY, + ), + DaxDialogIntroOption( + optionText = context.getString(R.string.onboardingSitesDaxDialogOption4), + iconRes = drawable.ic_wand_16, + link = site4Query, + pixel = ONBOARDING_VISIT_SITE_SURPRISE_ME, + ), + ) + } + + companion object { + const val FILENAME = "com.duckduckgo.app.onboarding.settings" + const val ONBOARDING_JOURNEY = "onboardingJourney" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt index 5c0e806220d7..643b9d8867c2 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt @@ -25,7 +25,6 @@ import com.duckduckgo.app.browser.databinding.ActivityOnboardingBinding import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope -import com.duckduckgo.mobile.android.R @InjectWith(ActivityScope::class) class OnboardingActivity : DuckDuckGoActivity() { @@ -39,13 +38,8 @@ class OnboardingActivity : DuckDuckGoActivity() { private val viewPager get() = binding.viewPager - private var forceLightTheme: Boolean = true - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - forceLightTheme = intent.getBooleanExtra(EXTRA_FORCE_LIGHT_THEME, true) - val onboardingTheme = if (forceLightTheme) R.style.Theme_DuckDuckGo_Light else R.style.Theme_DuckDuckGo - setTheme(onboardingTheme) setContentView(binding.root) configurePager() } @@ -59,7 +53,7 @@ class OnboardingActivity : DuckDuckGoActivity() { } } - fun onOnboardingDone() { + private fun onOnboardingDone() { viewModel.onOnboardingDone() startActivity(BrowserActivity.intent(this@OnboardingActivity)) finish() @@ -85,12 +79,8 @@ class OnboardingActivity : DuckDuckGoActivity() { companion object { - private const val EXTRA_FORCE_LIGHT_THEME = "FORCE_LIGHT_THEME" - - fun intent(context: Context, forceLightTheme: Boolean): Intent { - return Intent(context, OnboardingActivity::class.java).apply { - putExtra(EXTRA_FORCE_LIGHT_THEME, forceLightTheme) - } + fun intent(context: Context): Intent { + return Intent(context, OnboardingActivity::class.java) } } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageBuilder.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageBuilder.kt index b139c81459dd..b00635400b8a 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageBuilder.kt @@ -18,23 +18,19 @@ package com.duckduckgo.app.onboarding.ui import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPage import com.duckduckgo.app.onboarding.ui.page.WelcomePage -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage interface OnboardingPageBuilder { - fun buildWelcomePage(): WelcomePage - fun buildExperimentWelcomePage(): ExperimentWelcomePage + fun buildExperimentWelcomePage(): WelcomePage fun buildDefaultBrowserPage(): DefaultBrowserPage sealed class OnboardingPageBlueprint { - object DefaultBrowserBlueprint : OnboardingPageBlueprint() - object WelcomeBlueprint : OnboardingPageBlueprint() - object ExperimentWelcomeBluePrint : OnboardingPageBlueprint() + data object DefaultBrowserBlueprint : OnboardingPageBlueprint() + data object ExperimentWelcomeBluePrint : OnboardingPageBlueprint() } } class OnboardingFragmentPageBuilder : OnboardingPageBuilder { - override fun buildWelcomePage() = WelcomePage() - override fun buildExperimentWelcomePage() = ExperimentWelcomePage() + override fun buildExperimentWelcomePage() = WelcomePage() override fun buildDefaultBrowserPage() = DefaultBrowserPage() } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt index 3a2774ef8701..5b6a9059b3bf 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt @@ -21,12 +21,9 @@ import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.onboarding.ui.OnboardingPageBuilder.OnboardingPageBlueprint import com.duckduckgo.app.onboarding.ui.OnboardingPageBuilder.OnboardingPageBlueprint.DefaultBrowserBlueprint import com.duckduckgo.app.onboarding.ui.OnboardingPageBuilder.OnboardingPageBlueprint.ExperimentWelcomeBluePrint -import com.duckduckgo.app.onboarding.ui.OnboardingPageBuilder.OnboardingPageBlueprint.WelcomeBlueprint import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPage import com.duckduckgo.app.onboarding.ui.page.OnboardingPageFragment import com.duckduckgo.app.onboarding.ui.page.WelcomePage -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager interface OnboardingPageManager { fun pageCount(): Int @@ -38,7 +35,6 @@ class OnboardingPageManagerWithTrackerBlocking( private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog, private val onboardingPageBuilder: OnboardingPageBuilder, private val defaultWebBrowserCapability: DefaultBrowserDetector, - private val extendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager, ) : OnboardingPageManager { private val pages = mutableListOf() @@ -48,11 +44,7 @@ class OnboardingPageManagerWithTrackerBlocking( override fun buildPageBlueprints() { pages.clear() - if (extendedOnboardingExperimentVariantManager.isComparisonChartEnabled()) { - pages.add(ExperimentWelcomeBluePrint) - } else { - pages.add(WelcomeBlueprint) - } + pages.add(ExperimentWelcomeBluePrint) if (shouldShowDefaultBrowserPage()) { pages.add((DefaultBrowserBlueprint)) @@ -61,7 +53,6 @@ class OnboardingPageManagerWithTrackerBlocking( override fun buildPage(position: Int): OnboardingPageFragment? { return when (pages.getOrNull(position)) { - is WelcomeBlueprint -> buildWelcomePage() is ExperimentWelcomeBluePrint -> buildExperimentWelcomePage() is DefaultBrowserBlueprint -> buildDefaultBrowserPage() else -> null @@ -78,11 +69,7 @@ class OnboardingPageManagerWithTrackerBlocking( return onboardingPageBuilder.buildDefaultBrowserPage() } - private fun buildWelcomePage(): WelcomePage { - return onboardingPageBuilder.buildWelcomePage() - } - - private fun buildExperimentWelcomePage(): ExperimentWelcomePage { + private fun buildExperimentWelcomePage(): WelcomePage { return onboardingPageBuilder.buildExperimentWelcomePage() } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt index af442a43c000..3aa3e8788b55 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,114 +18,100 @@ package com.duckduckgo.app.onboarding.ui.page import android.Manifest import android.annotation.SuppressLint -import android.app.Activity.RESULT_OK +import android.app.Activity import android.content.Intent import android.graphics.Color import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.view.WindowManager import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.res.ResourcesCompat import androidx.core.view.ViewCompat import androidx.core.view.ViewPropertyAnimatorCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.R -import com.duckduckgo.app.browser.databinding.ContentOnboardingWelcomeBinding +import com.duckduckgo.app.browser.databinding.ContentOnboardingWelcomePageBinding +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType.CELEBRATION +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType.COMPARISON_CHART +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType.INITIAL +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowComparisonChart +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowDefaultBrowserDialog +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowSuccessDialog import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding -import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.FragmentViewModelFactory import com.duckduckgo.common.utils.extensions.html import com.duckduckgo.di.scopes.FragmentScope import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import nl.dionsegijn.konfetti.models.Shape +import nl.dionsegijn.konfetti.models.Size -@ExperimentalCoroutinesApi @InjectWith(FragmentScope::class) -class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome) { +class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_page) { @Inject - lateinit var viewModelFactory: WelcomePageViewModelFactory + lateinit var viewModelFactory: FragmentViewModelFactory @Inject lateinit var appBuildConfig: AppBuildConfig - @Inject - lateinit var dispatcherProvider: DispatcherProvider - - private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { - // In case of screen rotation while the notifications permissions prompt is shown on screen a DENY result is received - // as the dialog gets automatically dismissed and recreated. Proceed with the welcome animation only if the dialog is not - // displayed on top of the onboarding. - if (view?.windowVisibility == View.VISIBLE) { - // Nothing to do at this point with the result. Proceed with the welcome animation. - scheduleWelcomeAnimation(ANIMATION_DELAY_AFTER_NOTIFICATIONS_PERMISSIONS_HANDLED) - } + private val binding: ContentOnboardingWelcomePageBinding by viewBinding() + private val viewModel by lazy { + ViewModelProvider(this, viewModelFactory)[WelcomePageViewModel::class.java] } private var ctaText: String = "" + private var hikerAnimation: ViewPropertyAnimatorCompat? = null private var welcomeAnimation: ViewPropertyAnimatorCompat? = null private var typingAnimation: ViewPropertyAnimatorCompat? = null private var welcomeAnimationFinished = false - // we use replay = 0 because we don't want to emit the last value upon subscription - private val events = MutableSharedFlow(replay = 0, extraBufferCapacity = 1) - - private val welcomePageViewModel: WelcomePageViewModel by lazy { - ViewModelProvider(this, viewModelFactory)[WelcomePageViewModel::class.java] + private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted -> + if (permissionGranted) { + viewModel.notificationRuntimePermissionGranted() + } + if (view?.windowVisibility == View.VISIBLE) { + scheduleWelcomeAnimation(ANIMATION_DELAY_AFTER_NOTIFICATIONS_PERMISSIONS_HANDLED) + } } - private val binding: ContentOnboardingWelcomeBinding by viewBinding() + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val binding = ContentOnboardingWelcomePageBinding.inflate(inflater, container, false) + viewModel.commands.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).onEach { + when (it) { + is ShowComparisonChart -> configureDaxCta(COMPARISON_CHART) + is ShowDefaultBrowserDialog -> showDefaultBrowserDialog(it.intent) + is ShowSuccessDialog -> configureDaxCta(CELEBRATION) + is Finish -> onContinuePressed() + } + }.launchIn(lifecycleScope) + return binding.root + } override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - - configureDaxCta() requestNotificationsPermissions() setSkipAnimationListener() - - lifecycleScope.launch { - events - .flatMapLatest { welcomePageViewModel.reduce(it) } - .flowOn(dispatcherProvider.io()) - .collect(::render) - } - } - - @SuppressLint("InlinedApi") - private fun requestNotificationsPermissions() { - if (appBuildConfig.sdkInt >= android.os.Build.VERSION_CODES.TIRAMISU) { - requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) - } else { - scheduleWelcomeAnimation() - } - } - - private fun render(state: WelcomePageView.State) { - when (state) { - WelcomePageView.State.Idle -> {} - is WelcomePageView.State.ShowDefaultBrowserDialog -> { - showDefaultBrowserDialog(state.intent) - } - WelcomePageView.State.Finish -> { - onContinuePressed() - } - } - } - - private fun event(event: WelcomePageView.Event) { - lifecycleScope.launch(dispatcherProvider.io()) { - events.emit(event) - } - } - - private fun showDefaultBrowserDialog(intent: Intent) { - startActivityForResult(intent, DEFAULT_BROWSER_ROLE_MANAGER_DIALOG) } override fun onResume() { @@ -145,33 +131,81 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome) data: Intent?, ) { if (requestCode == DEFAULT_BROWSER_ROLE_MANAGER_DIALOG) { - if (resultCode == RESULT_OK) { - event(WelcomePageView.Event.OnDefaultBrowserSet) + if (resultCode == Activity.RESULT_OK) { + viewModel.onDefaultBrowserSet() } else { - event(WelcomePageView.Event.OnDefaultBrowserNotSet) + viewModel.onDefaultBrowserNotSet() } } else { super.onActivityResult(requestCode, resultCode, data) } } - private fun applyFullScreenFlags() { - activity?.window?.apply { - clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - decorView.systemUiVisibility += View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - statusBarColor = Color.TRANSPARENT - navigationBarColor = Color.BLACK + @SuppressLint("InlinedApi") + private fun requestNotificationsPermissions() { + if (appBuildConfig.sdkInt >= android.os.Build.VERSION_CODES.TIRAMISU) { + viewModel.notificationRuntimePermissionRequested() + requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + scheduleWelcomeAnimation() } - ViewCompat.requestApplyInsets(binding.longDescriptionContainer) } - private fun configureDaxCta() { + private fun configureDaxCta(onboardingDialogType: PreOnboardingDialogType) { context?.let { - ctaText = it.getString(R.string.onboardingDaxText) - binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) + viewModel.onDialogShown(onboardingDialogType) + when (onboardingDialogType) { + INITIAL -> { + ctaText = it.getString(R.string.preOnboardingDaxDialog1Title) + binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) + binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) + binding.daxDialogCta.daxDialogContentImage.gone() + + scheduleTypingAnimation { + binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog1Button) + binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(INITIAL) } + ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION + } + } + + COMPARISON_CHART -> { + binding.daxDialogCta.dialogTextCta.text = "" + TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) + ctaText = it.getString(R.string.preOnboardingDaxDialog2Title) + binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) + binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) + binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA + binding.daxDialogCta.comparisonChart.root.show() + binding.daxDialogCta.comparisonChart.root.alpha = MIN_ALPHA + + scheduleTypingAnimation { + binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog2Button) + binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(COMPARISON_CHART) } + ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION + ViewCompat.animate(binding.daxDialogCta.comparisonChart.root).alpha(MAX_ALPHA).duration = ANIMATION_DURATION + } + } + + CELEBRATION -> { + binding.daxDialogCta.dialogTextCta.text = "" + binding.daxDialogCta.comparisonChart.root.gone() + binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA + ctaText = it.getString(R.string.preOnboardingDaxDialog3Title) + binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) + binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) + binding.daxDialogCta.daxDialogContentImage.alpha = MIN_ALPHA + binding.daxDialogCta.daxDialogContentImage.show() + binding.daxDialogCta.daxDialogContentImage.setImageResource(R.drawable.ic_success_128) + launchKonfetti() + + scheduleTypingAnimation { + ViewCompat.animate(binding.daxDialogCta.daxDialogContentImage).alpha(MAX_ALPHA).duration = ANIMATION_DURATION + binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog3Button) + binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(CELEBRATION) } + ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION + } + } + } } } @@ -181,6 +215,7 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome) finishTypingAnimation() } else if (!welcomeAnimationFinished) { welcomeAnimation?.cancel() + hikerAnimation?.cancel() scheduleWelcomeAnimation(0L) } welcomeAnimationFinished = true @@ -188,33 +223,78 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome) } private fun scheduleWelcomeAnimation(startDelay: Long = ANIMATION_DELAY) { + ViewCompat.animate(binding.foregroundImageView) + .alpha(MIN_ALPHA) + .setDuration(ANIMATION_DURATION).startDelay = startDelay welcomeAnimation = ViewCompat.animate(binding.welcomeContent as View) .alpha(MIN_ALPHA) .setDuration(ANIMATION_DURATION) .setStartDelay(startDelay) .withEndAction { - typingAnimation = ViewCompat.animate(binding.daxDialogCta.daxCtaContainer) - .alpha(MAX_ALPHA) - .setDuration(ANIMATION_DURATION) - .withEndAction { - welcomeAnimationFinished = true - binding.daxDialogCta.dialogTextCta.startTypingAnimation(ctaText) - setPrimaryCtaListenerAfterWelcomeAlphaAnimation() - } + configureDaxCta(INITIAL) + } + } + + private fun scheduleTypingAnimation(afterAnimation: () -> Unit = {}) { + typingAnimation = ViewCompat.animate(binding.daxDialogCta.daxCtaContainer) + .alpha(MAX_ALPHA) + .setDuration(ANIMATION_DURATION) + .withEndAction { + welcomeAnimationFinished = true + binding.daxDialogCta.dialogTextCta.startTypingAnimation(ctaText, afterAnimation = afterAnimation) } } private fun finishTypingAnimation() { welcomeAnimation?.cancel() - binding.daxDialogCta.dialogTextCta.finishAnimation() - setPrimaryCtaListenerAfterWelcomeAlphaAnimation() + hikerAnimation?.cancel() + } + + private fun showDefaultBrowserDialog(intent: Intent) { + startActivityForResult(intent, DEFAULT_BROWSER_ROLE_MANAGER_DIALOG) } - private fun setPrimaryCtaListenerAfterWelcomeAlphaAnimation() { - binding.daxDialogCta.primaryCta.setOnClickListener { event(WelcomePageView.Event.OnPrimaryCtaClicked) } + private fun launchKonfetti() { + val magenta = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.magenta, null) + val blue = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.blue30, null) + val purple = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.purple, null) + val green = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.green, null) + val yellow = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.yellow, null) + + val displayWidth = resources.displayMetrics.widthPixels + + binding.setAsDefaultKonfetti.build() + .addColors(magenta, blue, purple, green, yellow) + .setDirection(0.0, 359.0) + .setSpeed(4f, 9f) + .setFadeOutEnabled(true) + .setTimeToLive(1500L) + .addShapes(Shape.Rectangle(1f)) + .addSizes(Size(8)) + .setPosition(displayWidth / 2f, displayWidth / 2f, -50f, -50f) + .streamFor(60, 2000L) + } + + private fun applyFullScreenFlags() { + activity?.window?.apply { + clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + decorView.systemUiVisibility += View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + statusBarColor = Color.TRANSPARENT + navigationBarColor = Color.BLACK + } + ViewCompat.requestApplyInsets(binding.longDescriptionContainer) } companion object { + + enum class PreOnboardingDialogType { + INITIAL, + COMPARISON_CHART, + CELEBRATION, + } + private const val MIN_ALPHA = 0f private const val MAX_ALPHA = 1f private const val ANIMATION_DURATION = 400L diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt index 12e7e4864719..8096a6726f43 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,95 +18,143 @@ package com.duckduckgo.app.onboarding.ui.page import android.annotation.SuppressLint import android.content.Context +import android.content.Intent import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType.CELEBRATION +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType.COMPARISON_CHART +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType.INITIAL +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowComparisonChart +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowDefaultBrowserDialog +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowSuccessDialog +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.NOTIFICATION_RUNTIME_PERMISSION_SHOWN +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.PREONBOARDING_AFFIRMATION_SHOWN +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.PREONBOARDING_AFFIRMATION_SHOWN_UNIQUE +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.PREONBOARDING_CHOOSE_BROWSER_PRESSED +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.PREONBOARDING_COMPARISON_CHART_SHOWN +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.PREONBOARDING_INTRO_SHOWN +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelName.PREONBOARDING_INTRO_SHOWN_UNIQUE +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel.PixelParameter import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE +import com.duckduckgo.di.scopes.FragmentScope +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch @SuppressLint("StaticFieldLeak") -class WelcomePageViewModel( - private val appInstallStore: AppInstallStore, +@ContributesViewModel(FragmentScope::class) +class WelcomePageViewModel @Inject constructor( + private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog, private val context: Context, private val pixel: Pixel, - private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog, + private val appInstallStore: AppInstallStore, ) : ViewModel() { - fun reduce(event: WelcomePageView.Event): Flow { - return when (event) { - WelcomePageView.Event.OnPrimaryCtaClicked -> onPrimaryCtaClicked() - WelcomePageView.Event.OnDefaultBrowserSet -> onDefaultBrowserSet() - WelcomePageView.Event.OnDefaultBrowserNotSet -> onDefaultBrowserNotSet() - } + private val _commands = Channel(1, DROP_OLDEST) + val commands: Flow = _commands.receiveAsFlow() + + sealed interface Command { + data object ShowComparisonChart : Command + data class ShowDefaultBrowserDialog(val intent: Intent) : Command + data object ShowSuccessDialog : Command + data object Finish : Command } - private fun onPrimaryCtaClicked(): Flow = flow { - if (defaultRoleBrowserDialog.shouldShowDialog()) { - val intent = defaultRoleBrowserDialog.createIntent(context) - if (intent != null) { - emit(WelcomePageView.State.ShowDefaultBrowserDialog(intent)) - } else { - pixel.fire(AppPixelName.DEFAULT_BROWSER_DIALOG_NOT_SHOWN) - emit(WelcomePageView.State.Finish) + fun onPrimaryCtaClicked(currentDialog: PreOnboardingDialogType) { + when (currentDialog) { + INITIAL -> { + viewModelScope.launch { + _commands.send(ShowComparisonChart) + } + } + + COMPARISON_CHART -> { + viewModelScope.launch { + val isDDGDefaultBrowser = + if (defaultRoleBrowserDialog.shouldShowDialog()) { + val intent = defaultRoleBrowserDialog.createIntent(context) + if (intent != null) { + _commands.send(ShowDefaultBrowserDialog(intent)) + } else { + pixel.fire(AppPixelName.DEFAULT_BROWSER_DIALOG_NOT_SHOWN) + _commands.send(Finish) + } + false + } else { + _commands.send(Finish) + true + } + pixel.fire( + PREONBOARDING_CHOOSE_BROWSER_PRESSED, + mapOf(PixelParameter.DEFAULT_BROWSER to isDDGDefaultBrowser.toString()), + ) + } + } + + CELEBRATION -> { + viewModelScope.launch { + _commands.send(Finish) + } } - } else { - emit(WelcomePageView.State.Finish) } } - private fun onDefaultBrowserSet(): Flow = flow { + fun onDefaultBrowserSet() { defaultRoleBrowserDialog.dialogShown() - appInstallStore.defaultBrowser = true + pixel.fire(AppPixelName.DEFAULT_BROWSER_SET, mapOf(Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString())) - pixel.fire( - AppPixelName.DEFAULT_BROWSER_SET, - mapOf( - Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(), - ), - ) - - emit(WelcomePageView.State.Finish) + viewModelScope.launch { + _commands.send(ShowSuccessDialog) + } } - private fun onDefaultBrowserNotSet(): Flow = flow { + fun onDefaultBrowserNotSet() { defaultRoleBrowserDialog.dialogShown() - appInstallStore.defaultBrowser = false + pixel.fire(AppPixelName.DEFAULT_BROWSER_NOT_SET, mapOf(Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString())) - pixel.fire( - AppPixelName.DEFAULT_BROWSER_NOT_SET, - mapOf( - Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(), - ), - ) + viewModelScope.launch { + _commands.send(Finish) + } + } - emit(WelcomePageView.State.Finish) + fun notificationRuntimePermissionRequested() { + pixel.fire(NOTIFICATION_RUNTIME_PERMISSION_SHOWN) } -} -@Suppress("UNCHECKED_CAST") -class WelcomePageViewModelFactory( - private val appInstallStore: AppInstallStore, - private val context: Context, - private val pixel: Pixel, - private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog, -) : ViewModelProvider.NewInstanceFactory() { + fun notificationRuntimePermissionGranted() { + pixel.fire( + AppPixelName.NOTIFICATIONS_ENABLED, + mapOf(PixelParameter.FROM_ONBOARDING to true.toString()), + ) + } - override fun create(modelClass: Class): T { - return with(modelClass) { - when { - isAssignableFrom(WelcomePageViewModel::class.java) -> WelcomePageViewModel( - appInstallStore, - context, - pixel, - defaultRoleBrowserDialog, - ) - else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + fun onDialogShown(onboardingDialogType: PreOnboardingDialogType) { + when (onboardingDialogType) { + INITIAL -> { + pixel.fire(PREONBOARDING_INTRO_SHOWN) + pixel.fire(PREONBOARDING_INTRO_SHOWN_UNIQUE, type = UNIQUE) } - } as T + COMPARISON_CHART -> { + pixel.fire(PREONBOARDING_COMPARISON_CHART_SHOWN) + pixel.fire(PixelName.PREONBOARDING_COMPARISON_CHART_SHOWN_UNIQUE, type = UNIQUE) + } + CELEBRATION -> { + pixel.fire(PREONBOARDING_AFFIRMATION_SHOWN) + pixel.fire(PREONBOARDING_AFFIRMATION_SHOWN_UNIQUE, type = UNIQUE) + } + } } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentDaxBubbleCardView.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentDaxBubbleCardView.kt deleted file mode 100644 index ed07b80b39c8..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentDaxBubbleCardView.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.onboarding.ui.page.experiment - -import android.content.Context -import android.content.res.ColorStateList -import android.util.AttributeSet -import com.duckduckgo.common.ui.view.getColorFromAttr -import com.duckduckgo.mobile.android.R -import com.google.android.material.card.MaterialCardView -import com.google.android.material.shape.EdgeTreatment -import com.google.android.material.shape.ShapeAppearanceModel -import com.google.android.material.shape.ShapePath - -class ExperimentDaxBubbleCardView -@JvmOverloads -constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.cardViewStyle, -) : MaterialCardView(context, attrs, defStyleAttr) { - - init { - val cornderRadius = resources.getDimension(R.dimen.mediumShapeCornerRadius) - val cornerSize = resources.getDimension(R.dimen.daxBubbleDialogEdge) - val distanceFromEdge = resources.getDimension(R.dimen.daxBubbleDialogDistanceFromEdge) - val edgeTreatment = ExperimentDaxBubbleEdgeTreatment(cornerSize, distanceFromEdge) - - setCardBackgroundColor(ColorStateList.valueOf(context.getColorFromAttr(R.attr.daxColorSurface))) - shapeAppearanceModel = ShapeAppearanceModel.builder() - .setAllCornerSizes(cornderRadius) - .setLeftEdge(edgeTreatment) - .build() - } - - class ExperimentDaxBubbleEdgeTreatment( - private val size: Float, - private val distanceFromEdge: Float, - ) : EdgeTreatment() { - override fun getEdgePath( - length: Float, - center: Float, - interpolation: Float, - shapePath: ShapePath, - ) { - val d = length - distanceFromEdge - shapePath.lineTo(d - size * interpolation, 0f) - shapePath.lineTo(d, -size * interpolation) - shapePath.lineTo(d + size * interpolation, 0f) - shapePath.lineTo(length, 0f) - } - } -} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentWelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentWelcomePage.kt deleted file mode 100644 index 0a2d125c399f..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentWelcomePage.kt +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.onboarding.ui.page.experiment - -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.graphics.Color -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.res.ResourcesCompat -import androidx.core.view.ViewCompat -import androidx.core.view.ViewPropertyAnimatorCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import androidx.transition.AutoTransition -import androidx.transition.TransitionManager -import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.app.browser.R -import com.duckduckgo.app.browser.databinding.ContentOnboardingWelcomeExperimentBinding -import com.duckduckgo.app.onboarding.ui.page.OnboardingPageFragment -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType.CELEBRATION -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType.COMPARISON_CHART -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType.INITIAL -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.Finish -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.ShowComparisonChart -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.ShowDefaultBrowserDialog -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.ShowSuccessDialog -import com.duckduckgo.appbuildconfig.api.AppBuildConfig -import com.duckduckgo.common.ui.view.gone -import com.duckduckgo.common.ui.view.show -import com.duckduckgo.common.ui.viewbinding.viewBinding -import com.duckduckgo.common.utils.FragmentViewModelFactory -import com.duckduckgo.common.utils.extensions.html -import com.duckduckgo.di.scopes.FragmentScope -import javax.inject.Inject -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import nl.dionsegijn.konfetti.models.Shape -import nl.dionsegijn.konfetti.models.Size - -@InjectWith(FragmentScope::class) -class ExperimentWelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_experiment) { - - @Inject - lateinit var viewModelFactory: FragmentViewModelFactory - - @Inject - lateinit var appBuildConfig: AppBuildConfig - - private val binding: ContentOnboardingWelcomeExperimentBinding by viewBinding() - private val viewModel by lazy { - ViewModelProvider(this, viewModelFactory)[ExperimentWelcomePageViewModel::class.java] - } - - private var ctaText: String = "" - private var hikerAnimation: ViewPropertyAnimatorCompat? = null - private var welcomeAnimation: ViewPropertyAnimatorCompat? = null - private var typingAnimation: ViewPropertyAnimatorCompat? = null - private var welcomeAnimationFinished = false - - private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted -> - if (permissionGranted) { - viewModel.notificationRuntimePermissionGranted() - } - if (view?.windowVisibility == View.VISIBLE) { - scheduleWelcomeAnimation(ANIMATION_DELAY_AFTER_NOTIFICATIONS_PERMISSIONS_HANDLED) - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - val binding = ContentOnboardingWelcomeExperimentBinding.inflate(inflater, container, false) - viewModel.commands.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).onEach { - when (it) { - is ShowComparisonChart -> configureDaxCta(COMPARISON_CHART) - is ShowDefaultBrowserDialog -> showDefaultBrowserDialog(it.intent) - is ShowSuccessDialog -> configureDaxCta(CELEBRATION) - is Finish -> onContinuePressed() - } - }.launchIn(lifecycleScope) - return binding.root - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - requestNotificationsPermissions() - setSkipAnimationListener() - } - - override fun onResume() { - super.onResume() - applyFullScreenFlags() - } - - override fun onDestroyView() { - super.onDestroyView() - welcomeAnimation?.cancel() - typingAnimation?.cancel() - } - - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent?, - ) { - if (requestCode == DEFAULT_BROWSER_ROLE_MANAGER_DIALOG) { - if (resultCode == Activity.RESULT_OK) { - viewModel.onDefaultBrowserSet() - } else { - viewModel.onDefaultBrowserNotSet() - } - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - @SuppressLint("InlinedApi") - private fun requestNotificationsPermissions() { - if (appBuildConfig.sdkInt >= android.os.Build.VERSION_CODES.TIRAMISU) { - viewModel.notificationRuntimePermissionRequested() - requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) - } else { - scheduleWelcomeAnimation() - } - } - - private fun configureDaxCta(onboardingDialogType: PreOnboardingDialogType) { - context?.let { - viewModel.onDialogShown(onboardingDialogType) - when (onboardingDialogType) { - INITIAL -> { - ctaText = it.getString(R.string.preOnboardingDaxDialog1Title) - binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) - binding.daxDialogCta.experimentDialogContentImage.gone() - - scheduleTypingAnimation { - binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog1Button) - binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(INITIAL) } - ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION - } - } - - COMPARISON_CHART -> { - binding.daxDialogCta.dialogTextCta.text = "" - TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) - ctaText = it.getString(R.string.preOnboardingDaxDialog2Title) - binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) - binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA - binding.daxDialogCta.comparisonChart.root.show() - binding.daxDialogCta.comparisonChart.root.alpha = MIN_ALPHA - - scheduleTypingAnimation { - binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog2Button) - binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(COMPARISON_CHART) } - ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION - ViewCompat.animate(binding.daxDialogCta.comparisonChart.root).alpha(MAX_ALPHA).duration = ANIMATION_DURATION - } - } - - CELEBRATION -> { - binding.daxDialogCta.dialogTextCta.text = "" - binding.daxDialogCta.comparisonChart.root.gone() - binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA - ctaText = it.getString(R.string.preOnboardingDaxDialog3Title) - binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) - binding.daxDialogCta.experimentDialogContentImage.alpha = MIN_ALPHA - binding.daxDialogCta.experimentDialogContentImage.show() - binding.daxDialogCta.experimentDialogContentImage.setImageResource(R.drawable.ic_success_128) - launchKonfetti() - - scheduleTypingAnimation { - ViewCompat.animate(binding.daxDialogCta.experimentDialogContentImage).alpha(MAX_ALPHA).duration = ANIMATION_DURATION - binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog3Button) - binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(CELEBRATION) } - ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION - } - } - } - } - } - - private fun setSkipAnimationListener() { - binding.longDescriptionContainer.setOnClickListener { - if (binding.daxDialogCta.dialogTextCta.hasAnimationStarted()) { - finishTypingAnimation() - } else if (!welcomeAnimationFinished) { - welcomeAnimation?.cancel() - hikerAnimation?.cancel() - scheduleWelcomeAnimation(0L) - } - welcomeAnimationFinished = true - } - } - - private fun scheduleWelcomeAnimation(startDelay: Long = ANIMATION_DELAY) { - ViewCompat.animate(binding.foregroundImageView) - .alpha(MIN_ALPHA) - .setDuration(ANIMATION_DURATION).startDelay = startDelay - welcomeAnimation = ViewCompat.animate(binding.welcomeContent as View) - .alpha(MIN_ALPHA) - .setDuration(ANIMATION_DURATION) - .setStartDelay(startDelay) - .withEndAction { - configureDaxCta(INITIAL) - } - } - - private fun scheduleTypingAnimation(afterAnimation: () -> Unit = {}) { - typingAnimation = ViewCompat.animate(binding.daxDialogCta.daxCtaContainer) - .alpha(MAX_ALPHA) - .setDuration(ANIMATION_DURATION) - .withEndAction { - welcomeAnimationFinished = true - binding.daxDialogCta.dialogTextCta.startTypingAnimation(ctaText, afterAnimation = afterAnimation) - } - } - - private fun finishTypingAnimation() { - welcomeAnimation?.cancel() - hikerAnimation?.cancel() - } - - private fun showDefaultBrowserDialog(intent: Intent) { - startActivityForResult(intent, DEFAULT_BROWSER_ROLE_MANAGER_DIALOG) - } - - private fun launchKonfetti() { - val magenta = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.magenta, null) - val blue = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.blue30, null) - val purple = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.purple, null) - val green = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.green, null) - val yellow = ResourcesCompat.getColor(resources, com.duckduckgo.mobile.android.R.color.yellow, null) - - val displayWidth = resources.displayMetrics.widthPixels - - binding.setAsDefaultKonfetti.build() - .addColors(magenta, blue, purple, green, yellow) - .setDirection(0.0, 359.0) - .setSpeed(4f, 9f) - .setFadeOutEnabled(true) - .setTimeToLive(1500L) - .addShapes(Shape.Rectangle(1f)) - .addSizes(Size(8)) - .setPosition(displayWidth / 2f, displayWidth / 2f, -50f, -50f) - .streamFor(60, 2000L) - } - - private fun applyFullScreenFlags() { - activity?.window?.apply { - clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - decorView.systemUiVisibility += View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - statusBarColor = Color.TRANSPARENT - navigationBarColor = Color.BLACK - } - ViewCompat.requestApplyInsets(binding.longDescriptionContainer) - } - - companion object { - - enum class PreOnboardingDialogType { - INITIAL, - COMPARISON_CHART, - CELEBRATION, - } - - private const val MIN_ALPHA = 0f - private const val MAX_ALPHA = 1f - private const val ANIMATION_DURATION = 400L - private const val ANIMATION_DELAY = 1400L - private const val ANIMATION_DELAY_AFTER_NOTIFICATIONS_PERMISSIONS_HANDLED = 800L - - private const val DEFAULT_BROWSER_ROLE_MANAGER_DIALOG = 101 - } -} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentWelcomePageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentWelcomePageViewModel.kt deleted file mode 100644 index c76dc1777c76..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExperimentWelcomePageViewModel.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.onboarding.ui.page.experiment - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.app.global.DefaultRoleBrowserDialog -import com.duckduckgo.app.global.install.AppInstallStore -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType.CELEBRATION -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType.COMPARISON_CHART -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePage.Companion.PreOnboardingDialogType.INITIAL -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.Finish -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.ShowComparisonChart -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.ShowDefaultBrowserDialog -import com.duckduckgo.app.onboarding.ui.page.experiment.ExperimentWelcomePageViewModel.Command.ShowSuccessDialog -import com.duckduckgo.app.pixels.AppPixelName -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE -import com.duckduckgo.di.scopes.FragmentScope -import javax.inject.Inject -import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -@SuppressLint("StaticFieldLeak") -@ContributesViewModel(FragmentScope::class) -class ExperimentWelcomePageViewModel @Inject constructor( - private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog, - private val context: Context, - private val pixel: Pixel, - private val appInstallStore: AppInstallStore, -) : ViewModel() { - - private val _commands = Channel(1, DROP_OLDEST) - val commands: Flow = _commands.receiveAsFlow() - - sealed interface Command { - data object ShowComparisonChart : Command - data class ShowDefaultBrowserDialog(val intent: Intent) : Command - data object ShowSuccessDialog : Command - data object Finish : Command - } - - fun onPrimaryCtaClicked(currentDialog: PreOnboardingDialogType) { - when (currentDialog) { - INITIAL -> { - viewModelScope.launch { - _commands.send(ShowComparisonChart) - } - } - - COMPARISON_CHART -> { - viewModelScope.launch { - val isDDGDefaultBrowser = - if (defaultRoleBrowserDialog.shouldShowDialog()) { - val intent = defaultRoleBrowserDialog.createIntent(context) - if (intent != null) { - _commands.send(ShowDefaultBrowserDialog(intent)) - } else { - pixel.fire(AppPixelName.DEFAULT_BROWSER_DIALOG_NOT_SHOWN) - _commands.send(Finish) - } - false - } else { - _commands.send(Finish) - true - } - pixel.fire( - OnboardingExperimentPixel.PixelName.PREONBOARDING_CHOOSE_BROWSER_PRESSED, - mapOf(OnboardingExperimentPixel.PixelParameter.DEFAULT_BROWSER to isDDGDefaultBrowser.toString()), - ) - } - } - - CELEBRATION -> { - viewModelScope.launch { - _commands.send(Finish) - } - } - } - } - - fun onDefaultBrowserSet() { - defaultRoleBrowserDialog.dialogShown() - appInstallStore.defaultBrowser = true - pixel.fire(AppPixelName.DEFAULT_BROWSER_SET, mapOf(Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString())) - - viewModelScope.launch { - _commands.send(ShowSuccessDialog) - } - } - - fun onDefaultBrowserNotSet() { - defaultRoleBrowserDialog.dialogShown() - appInstallStore.defaultBrowser = false - pixel.fire(AppPixelName.DEFAULT_BROWSER_NOT_SET, mapOf(Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString())) - - viewModelScope.launch { - _commands.send(Finish) - } - } - - fun notificationRuntimePermissionRequested() { - pixel.fire(OnboardingExperimentPixel.PixelName.NOTIFICATION_RUNTIME_PERMISSION_SHOWN) - } - - fun notificationRuntimePermissionGranted() { - pixel.fire( - AppPixelName.NOTIFICATIONS_ENABLED, - mapOf(OnboardingExperimentPixel.PixelParameter.FROM_ONBOARDING to true.toString()), - ) - } - - fun onDialogShown(onboardingDialogType: PreOnboardingDialogType) { - when (onboardingDialogType) { - INITIAL -> { - pixel.fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_INTRO_SHOWN) - pixel.fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_INTRO_SHOWN_UNIQUE, type = UNIQUE) - } - COMPARISON_CHART -> { - pixel.fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_COMPARISON_CHART_SHOWN) - pixel.fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_COMPARISON_CHART_SHOWN_UNIQUE, type = UNIQUE) - } - CELEBRATION -> { - pixel.fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_AFFIRMATION_SHOWN) - pixel.fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_AFFIRMATION_SHOWN_UNIQUE, type = UNIQUE) - } - } - } -} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExtendedOnboardingExperimentVariantManager.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExtendedOnboardingExperimentVariantManager.kt deleted file mode 100644 index 0f0cf35c286b..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExtendedOnboardingExperimentVariantManager.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * 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. - */ - -package com.duckduckgo.app.onboarding.ui.page.experiment - -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.experiments.api.VariantConfig -import com.duckduckgo.experiments.api.VariantFilters -import com.duckduckgo.experiments.api.VariantManager -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject - -interface ExtendedOnboardingExperimentVariantManager { - fun setExperimentVariants() - fun isComparisonChartEnabled(): Boolean - fun isAestheticUpdatesEnabled(): Boolean -} - -@ContributesBinding(AppScope::class) -class ExtendedOnboardingExperimentVariantManagerImpl @Inject constructor( - private val variantManager: VariantManager, - private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles, -) : ExtendedOnboardingExperimentVariantManager { - - private val isExtendedOnboardingEnabled: Boolean = true - - override fun setExperimentVariants() { - val variants = listOf( - VariantConfig("ms", 1.0, VariantFilters(locale = listOf("en_US", "en_GB", "en_CA", "en_IN", "en_AU"))), - VariantConfig("mt", 1.0, VariantFilters(locale = listOf("en_US", "en_GB", "en_CA", "en_IN", "en_AU"))), - ) - variantManager.updateVariants(variants) - } - - override fun isComparisonChartEnabled(): Boolean { - val isRemoteFeatureEnabled = extendedOnboardingFeatureToggles.comparisonChart().isEnabled() - val isLocalFeatureEnabled = isExtendedOnboardingEnabled && variantManager.getVariantKey() == "mt" - return isRemoteFeatureEnabled || isLocalFeatureEnabled - } - - override fun isAestheticUpdatesEnabled(): Boolean { - return extendedOnboardingFeatureToggles.aestheticUpdates().isEnabled() - } -} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExtendedOnboardingFeatureToggles.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt similarity index 80% rename from app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExtendedOnboardingFeatureToggles.kt rename to app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt index 272dda622ceb..0c9e66f54672 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/ExtendedOnboardingFeatureToggles.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt @@ -14,26 +14,21 @@ * limitations under the License. */ -package com.duckduckgo.app.onboarding.ui.page.experiment +package com.duckduckgo.app.onboarding.ui.page.extendedonboarding import com.duckduckgo.anvil.annotations.ContributesRemoteFeature import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.feature.toggles.api.Toggle -import com.duckduckgo.feature.toggles.api.Toggle.Experiment @ContributesRemoteFeature( scope = AppScope::class, featureName = "extendedOnboarding", ) interface ExtendedOnboardingFeatureToggles { - @Toggle.DefaultValue(false) - fun self(): Toggle @Toggle.DefaultValue(false) - @Experiment - fun comparisonChart(): Toggle + fun self(): Toggle - @Toggle.DefaultValue(false) - @Experiment + @Toggle.DefaultValue(true) fun aestheticUpdates(): Toggle } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/OnboardingExperimentPixel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/OnboardingExperimentPixel.kt similarity index 97% rename from app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/OnboardingExperimentPixel.kt rename to app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/OnboardingExperimentPixel.kt index e8426b5110e0..a22db231aeac 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/experiment/OnboardingExperimentPixel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/OnboardingExperimentPixel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.onboarding.ui.page.experiment +package com.duckduckgo.app.onboarding.ui.page.extendedonboarding import com.duckduckgo.app.statistics.pixels.Pixel diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index 9158d9055ed8..00d285a36a65 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -241,7 +241,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { FIRE_DIALOG_PROMOTED_CLEAR_PRESSED("m_fdp_p"), FIRE_DIALOG_CLEAR_PRESSED("m_fd_p"), - FIRE_DIALOG_PROMOTED_CANCEL("m_fdp_c"), FIRE_DIALOG_CANCEL("m_fd_c"), FIRE_DIALOG_ANIMATION("m_fd_a"), diff --git a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt index b1e31b1a2f66..a4e889167bac 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt @@ -34,6 +34,8 @@ import javax.inject.Inject interface SettingsDataStore { var lastExecutedJobId: String? + + @Deprecated(message = "hideTips variable is deprecated and no longer available in onboarding") var hideTips: Boolean var autoCompleteSuggestionsEnabled: Boolean var appIcon: AppIcon diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index ce7f76d5b04e..1d58362a4c72 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -30,7 +30,6 @@ import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister -import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.downloads.DownloadsActivity import com.duckduckgo.app.global.events.db.UserEventsStore @@ -80,9 +79,6 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine @Inject lateinit var pixel: Pixel - @Inject - lateinit var ctaViewModel: CtaViewModel - @Inject lateinit var faviconManager: FaviconManager @@ -214,7 +210,6 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine val dialog = FireDialog( context = this, clearPersonalDataAction = clearPersonalDataAction, - ctaViewModel = ctaViewModel, pixel = pixel, settingsDataStore = settingsDataStore, userEventsStore = userEventsStore, diff --git a/app/src/main/res/drawable-hdpi/onboarding_background_large.png b/app/src/main/res/drawable-hdpi/onboarding_background_large.png deleted file mode 100644 index e9d5a6e85d03..000000000000 Binary files a/app/src/main/res/drawable-hdpi/onboarding_background_large.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/onboarding_background_small.png b/app/src/main/res/drawable-hdpi/onboarding_background_small.png deleted file mode 100644 index 198a6e7487f3..000000000000 Binary files a/app/src/main/res/drawable-hdpi/onboarding_background_small.png and /dev/null differ diff --git a/app/src/main/res/drawable-w600dp/onboarding_background_large.png b/app/src/main/res/drawable-w600dp/onboarding_background_large.png deleted file mode 100644 index 7e21c7d2d8c5..000000000000 Binary files a/app/src/main/res/drawable-w600dp/onboarding_background_large.png and /dev/null differ diff --git a/app/src/main/res/drawable/onboarding_background.xml b/app/src/main/res/drawable/onboarding_background.xml deleted file mode 100644 index e62fa3e1ef09..000000000000 --- a/app/src/main/res/drawable/onboarding_background.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/content_onboarding_welcome.xml b/app/src/main/res/layout-land/content_onboarding_welcome.xml deleted file mode 100644 index cbd033476bf6..000000000000 --- a/app/src/main/res/layout-land/content_onboarding_welcome.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/content_onboarding_welcome_experiment.xml b/app/src/main/res/layout-land/content_onboarding_welcome_page.xml similarity index 98% rename from app/src/main/res/layout-land/content_onboarding_welcome_experiment.xml rename to app/src/main/res/layout-land/content_onboarding_welcome_page.xml index 8730c2bd4616..fab0baed9fe8 100644 --- a/app/src/main/res/layout-land/content_onboarding_welcome_experiment.xml +++ b/app/src/main/res/layout-land/content_onboarding_welcome_page.xml @@ -77,7 +77,7 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/content_onboarding_welcome_experiment.xml b/app/src/main/res/layout/content_onboarding_welcome_page.xml similarity index 98% rename from app/src/main/res/layout/content_onboarding_welcome_experiment.xml rename to app/src/main/res/layout/content_onboarding_welcome_page.xml index bc58af148d82..40a9d8b40396 100644 --- a/app/src/main/res/layout/content_onboarding_welcome_experiment.xml +++ b/app/src/main/res/layout/content_onboarding_welcome_page.xml @@ -76,7 +76,7 @@ @@ -96,7 +96,7 @@ android:id="@+id/swipeRefreshContainer" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_below="@id/experimentDaxDialogContent"> + android:layout_below="@id/daxDialogOnboardingCtaContent"> diff --git a/app/src/main/res/layout/include_dax_dialog_intro_experiment_cta.xml b/app/src/main/res/layout/include_dax_dialog_intro_bubble_cta.xml similarity index 99% rename from app/src/main/res/layout/include_dax_dialog_intro_experiment_cta.xml rename to app/src/main/res/layout/include_dax_dialog_intro_bubble_cta.xml index 042ee08e6450..0ace4cfcb2e4 100644 --- a/app/src/main/res/layout/include_dax_dialog_intro_experiment_cta.xml +++ b/app/src/main/res/layout/include_dax_dialog_intro_bubble_cta.xml @@ -51,7 +51,7 @@ android:orientation="vertical"> @@ -82,8 +83,8 @@ - - + + > \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_search_suggestion.xml b/app/src/main/res/layout/item_autocomplete_search_suggestion.xml index 54230ac18fd5..d626eecde7a9 100644 --- a/app/src/main/res/layout/item_autocomplete_search_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_search_suggestion.xml @@ -41,7 +41,7 @@ - - - + android:layout_gravity="bottom"> - + \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index c0fb3aae7209..6f03ddfe1ade 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -843,10 +843,14 @@ Стартиране на сърфирането Опитайте търсене! Търсенето в DuckDuckGo винаги е анонимно. - как се казва „патица“ на испански - прогнозата на мощните патици + как се казва „патица“ на английски + как се казва „патица“ на испански + как се нарича патицата на английски + как се казва „патица“ на испански + актьори в аватар местното време Изненадайте ме! + рецепти за вечеря Опитайте да посетите сайт! Ще блокирам тракерите, за да не ви шпионират. espn.com @@ -868,5 +872,6 @@ Fire Button.

Изпробвайте го! ☝️️]]>
"Справихте се!" Запомнете: всеки път, когато сърфирате с мен, аз ще подрязвам крилцата на досадните реклами. 👌 + Готово! diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index afdd6ab55adb..88cec3687799 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -853,10 +853,14 @@ Spustit procházení Vyzkoušejte vyhledávání! Tvoje vyhledávání v DuckDuckGo je vždycky anonymní. - jak se řekne „kachna“ španělsky - herci z filmu Šampióni + jak se řekne anglicky „kachna“ + jak se řekne „kachna“ španělsky + jak se řekne anglicky kachna + jak se řekne španělsky „kachna“ + obsazení avatara aktuální počasí Překvap mě! + recepty na večeři Zkus přejít na nějaký web! Zablokuju trackery, aby tě nemohly šmírovat. espn.com @@ -882,5 +886,6 @@ Fire Button.

Zkus ji! ☝️️]]>
"A je to!" Pamatuj: Když se mnou na webu surfuješ, příšerné reklamy zaženeš. 👌 + Všechno je hotové! diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index c261a4b036ed..32dd301477ee 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -843,10 +843,14 @@ Start søgning Prøv at søge! Dine DuckDuckGo-søgninger er altid anonyme. - hvordan siger man \"and\" på spansk - mighty ducks film + hvordan siger man \"and\" på engelsk + hvordan siger man \"and\" på spansk + hvordan siger man and på engelsk + hvordan siger man \"and\" på spansk + skuespillere i avatar lokalt vejr Overrask mig! + middagsopskrifter Prøv at besøge en hjemmeside! Jeg blokerer trackere, så de ikke kan udspionere dig. espn.com @@ -868,5 +872,6 @@ Fire Button.

Prøv det! ☝️️]]>
"Du har forstået det!" Husk: hver gang du browser med mig, mister en uhyggelig annonce sine vinger. 👌 + Helt færdig! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f90186cc0150..53c20b19ef36 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -843,10 +843,14 @@ Mit dem Browsen beginnen Suche ausprobieren! Deine DuckDuckGo-Suchanfragen sind immer anonym. - wie sagt man „Ente“ auf Spanisch - Besetzung von Mighty Ducks + wie sagt man „Ente“ auf Englisch + wie sagt man „Ente“ auf Spanisch + wie sagt man Ente auf Englisch + wie sagt man „Ente“ auf Spanisch + Besetzung von Avatar Lokales Wetter Überrasche mich! + Dinner-Rezepte Versuche, eine Website zu besuchen! Ich blockiere Tracker, damit sie dich nicht ausspionieren können. espn.com @@ -868,5 +872,6 @@ Fire Button.

Versuch es doch mal! ☝️️]]>
"Du schaffst das!" Hinweis: Jedes Mal, wenn du mit mir browst, verliert eine gruselige Anzeige ihren Schrecken. 👌 + Alles erledigt! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index aaff316401ef..e4f45cee978e 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -843,10 +843,14 @@ Έναρξη περιήγησης Δοκίμασε μια αναζήτηση! Οι αναζητήσεις σας στο DuckDuckGo είναι πάντα ανώνυμες. - πώς μπορείτε να πείτε «duck» στα ισπανικά - Mighty Ducks Cast + πώς να πείτε «πάπια» στα αγγλικά + πώς μπορείτε να πείτε «duck» στα ισπανικά + πώς να πείτε πάπια στα αγγλικά + πώς μπορείτε να πείτε πάπια στα ισπανικά + οι ηθοποιοί του avatar τοπικός καιρός Κάνε μου έκπληξη! + συνταγές για δείπνο Δοκιμάστε να επισκεφτείτε έναν ιστότοπο! Θα εμποδίσω τις εφαρμογές παρακολούθησης ώστε να μην μπορούν να σας κατασκοπεύουν. espn.com @@ -868,5 +872,6 @@ Fire Button.

Δοκιμάστε το! ☝️️]]>
"Το έχετε!" Να θυμάστε: κάθε φορά που περιηγείστε μαζί μου, μια ανατριχιαστική διαφήμιση χάνει τη δύναμή της! 👌 + Όλα έτοιμα! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7dda35846ae3..04df56794e18 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -843,10 +843,14 @@ Empezar a navegar Prueba una búsqueda! Tus búsquedas en DuckDuckGo son siempre anónimas. - cómo se dice «pato» en inglés - reparto de Mighty Ducks + cómo se dice «pato» en inglés + cómo se dice «pato» en inglés + cómo se dice «pato» en inglés + cómo se dice «duck» en español + reparto de Avatar el tiempo local ¡Sorpréndeme! + recetas para la cena ¡Intenta visitar un sitio! Bloquearé los rastreadores para que no puedan espiarte. espn.com @@ -868,5 +872,6 @@ Fire Button.

¡Pruébalo! ☝️️]]>
"¡Lo estás haciendo muy bien!" Recuerda: cada vez que navegas conmigo corto las alas a un anuncio horrible. 👌 + ¡Todo listo! diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index e6705efc8ab0..ed5a441dd057 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -843,10 +843,14 @@ Alusta sirvimist Proovige otsingut! Sinu DuckDuckGo otsingud on alati anonüümsed. - kuidas öelda „part“ hispaania keeles - Mighty Ducks näitlejad + kuidas öelda „part“ inglise keeles + kuidas öelda „part“ hispaania keeles + kuidas öelda part inglise keeles + kuidas öelda part hispaania keeles + Avatari näitlejad kohalik ilm Üllata mind! + õhtusöögi retseptid Proovi külastada saiti! Ma blokeerin jälgijaid, et nad ei saaks sind luurata. espn.com @@ -868,5 +872,6 @@ Fire Button abil.

Proovi! ☝️️]]>
"Sa saad hakkama!" Pea meeles: iga kord kui minuga sirvid, kaotab jube reklaam oma tiivad. 👌 + Kõik valmis! diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 1306fc34078e..4f726f33a700 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -843,10 +843,14 @@ Aloita selailu Kokeile hakua! DuckDuckGo-hakusi ovat aina nimettömiä. - kuinka sanotaan \"duck\" espanjaksi - mighty ducks cast + miten sanotaan \"ankka\" englanniksi + kuinka sanotaan \"duck\" espanjaksi + miten sanotaan ankka englanniksi + miten sanotaan ankka espanjaksi + avatarmuotti paikallissää Yllätä minut! + illallisreseptejä Siirry sivustolle! Estän jäljittäjät, jotta ne eivät voi vakoilla sinua. espn.com @@ -868,5 +872,6 @@ Fire Button -painikkeella.

Kokeile nyt! ☝️️]]>
"Hyvin menee!" Muista, että joka kerta kun käytät minua selaamiseen, rasittavat mainokset katoavat. 👌 + Valmista! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9b694871d221..27be1a565b85 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -843,10 +843,14 @@ Commencer la navigation Essayez une recherche ! Vos recherches sur DuckDuckGo sont toujours anonymes. - comment dire « duck » en espagnol - casting de mighty ducks + comment dire « canard » en anglais + comment dire « duck » en espagnol + comment dire canard en anglais + comment dire « canard » en espagnol + casting d\'avatar météo locale Surprenez-moi ! + recettes pour le dîner Essayez de visiter un site ! Je bloquerai les traqueurs afin qu\'ils ne puissent pas vous espionner. espn.com @@ -868,5 +872,6 @@ Fire Button.

Essayez par vous-même ! ☝️️]]>
"Bien joué !" Pensez-y : chaque fois que vous naviguez avec moi, une publicité douteuse disparaît. 👌 + C\'est fait ! diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 8ed6aa052742..8c51c0cc0cdd 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -853,10 +853,14 @@ Započni pretraživanje Isprobajte pretragu! Tvoja su DuckDuckGo pretraživanja uvijek anonimna. - kako se kaže \"patka\" na španjolskom - moćne patke + kako se kaže \"patka\" na engleskom + kako se kaže \"patka\" na španjolskom + kako se kaže patka na engleskom + kako se kaže \"patka\" na španjolskom + uloge u filmu Avatar lokalno vrijeme Iznenadi me! + recepti za večeru Pokušaj posjetiti web-mjesto! Blokirat ću tragače kako te ne bi mogli špijunirati. espn.com @@ -882,5 +886,6 @@ Fire Buttona.

Probaj! ☝️️]]>
"Možeš ti to!" Zapamti: svaki put kada me koristiš za pregledavanje, grozne reklame odlaze u zaborav. 👌 + Gotovo! diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 03e8d4278df0..9e608fca4078 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -843,10 +843,14 @@ Böngészés indítása Keress rá! A DuckDuckGo-kereséseid mindig névtelenek. - hogyan mondják spanyolul, hogy „kacsa” - kerge kacsák szereposztása + hogyan mondják angolul, hogy „kacsa” + hogyan mondják spanyolul, hogy „kacsa” + hogyan mondják angolul, hogy kacsa + hogyan mondják spanyolul, hogy kacsa + avatar szereposztása helyi időjárás Lepj meg! + vacsorareceptek Próbálj meg ellátogatni egy webhelyre! Én blokkolom a nyomkövetőket, hogy ne tudjanak kémkedni utánad. espn.com @@ -868,5 +872,6 @@ Fire Button használatával.

Próbáld ki! ☝️️]]>
"Megvan, ez az!" Ne feledd: minden alkalommal, amikor velem böngészel, egy undok hirdetés elveszíti az erejét. 👌 + Minden kész! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5c54c5578b5e..b0e75446ad57 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -843,10 +843,14 @@ Inizia a navigare Prova una ricerca! Le tue ricerche su DuckDuckGo sono sempre anonime. - come si dice \"anatra\" in spagnolo - cast delle papere potenti + come si dice \"anatra\" in inglese + come si dice \"anatra\" in spagnolo + come si dice anatra in inglese + come si dice anatra in spagnolo + cast di avatar meteo locale Sorprendimi! + ricette per la cena Prova a visitare un sito! Bloccherò i sistemi di tracciamento in modo che non possano spiarti. espn.com @@ -868,5 +872,6 @@ Fire Button.

Provalo! ☝️️]]>
"Ben fatto!" Ricorda: quando navighi con me gli annunci inquietanti non possono seguirti. 👌 + Fatto! diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index ed9cff25ace9..cd2a2d6d5791 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -853,10 +853,14 @@ Pradėti naršyti Išmėgink paiešką! Jūsų „DuckDuckGo“ paieškos visada yra anoniminės. - kaip pasakyti „antis“ ispanų kalba - mighty ducks aktoriai + kaip pasakyti „antis“ anglų kalba + kaip pasakyti „antis“ ispanų kalba + kaip pasakyti antis anglų kalba + kaip pasakyti antis ispanų kalba + įsikūnijimo aktoriai vietinis oras Nustebink mane! + vakarienės receptai Pabandyk apsilankyti svetainėje! Užblokuosiu stebėjimo priemones, kad jos negalėtų tavęs šnipinėti. espn.com @@ -882,5 +886,6 @@ Fire Button.

Išbandykite! ☝️️]]>
"Atlikote!" Įsidėmėkite: kiekvieną kartą, kai naršai su manimi, bauginantis skelbimas praranda galią. 👌 + Viskas! diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 311ee12a3724..53d18c70a454 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -848,10 +848,14 @@ Sākt pārlūkošanu Izmēģini meklēt! Tavi DuckDuckGo meklējumi vienmēr ir anonīmi. - kā spāniski pateikt “pīle” - mighty ducks aktieri + kā angliski pateikt “pīle” + kā spāniski pateikt “pīle” + kā angliski pateikt “pīle” + kā spāniski pateikt “pīle” + avatara aktieru sastāvs vietējie laikapstākļi Pārsteidz mani! + vakariņu receptes Pamēģini apmeklēt kādu vietni! Es nobloķēšu izsekotājus, lai tie nevarētu tevi izspiegot. espn.com @@ -875,5 +879,6 @@ Fire Button.

Izmēģini! ☝️️]]>
"Izdevās!" Atceries: katru reizi, kad pārlūkosi kopā ar mani, uzmācīgās reklāmas zaudēs savu spēku! 👌 + Viss pabeigts! diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 3daf0498490a..f4ba311183f8 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -843,10 +843,14 @@ Begynn å surfe Prøv et søk! DuckDuckGo-søkene dine er alltid anonyme. - hvordan si «duck» på spansk - mighty ducks rolleinnehavere + hva heter «and» på engelsk + hvordan si «duck» på spansk + hva heter and på engelsk + hva heter and på spansk + rollebesetningen i avatar lokalt vær Overrask meg! + middagsoppskrifter Prøv å besøke et nettsted! Jeg blokkerer sporingsforsøk slik at de ikke kan spionere på deg. espn.com @@ -868,5 +872,6 @@ Fire Button.

Prøv det! ☝️️]]>
"Dette går bra!" Husk: Hver gang du surfer med meg, klippes vingene på en uhyggelig annonse. 👌 + Ferdig! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 552c646430c5..504636385cc0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -843,10 +843,14 @@ Beginnen met browsen Probeer een zoekopdracht! Je DuckDuckGo-zoekopdrachten zijn altijd anoniem. - hoe zeg je \'eend\' in het Spaans? - cast van Mighty Ducks + hoe zeg je \'eend\' in het Engels? + hoe zeg je \'eend\' in het Spaans? + hoe zeg je \'eend\' in het Engels? + hoe zeg je \'eend\' in het Spaans? + cast van avatar lokaal weer Verras me! + recepten voor het avondeten Bezoek eens een site! Ik blokkeer trackers zodat ze je niet kunnen bespioneren. espn.com @@ -868,5 +872,6 @@ Fire Button.

Probeer het maar! ☝️️]]>
"Je kunt het!" Denk eraan: elke keer als je met mij browset, verliest een enge advertentie zijn vleugels. 👌 + Helemaal klaar! diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d12bb706a3af..a1270dd75e31 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -853,10 +853,14 @@ Rozpocznij przeglądanie Spróbuj coś wyszukać! Wyszukiwania w DuckDuckGo zawsze są anonimowe. - jak się mówi „kaczka” po hiszpańsku - obsada potężnych kaczorów + jak się mówi „kaczka” po angielsku + jak się mówi „kaczka” po hiszpańsku + jak się mówi kaczka po angielsku + jak się mówi kaczka po hiszpańsku + obsada avatara pogoda lokalna Zaskocz mnie! + przepisy na obiad Spróbuj odwiedzić witrynę! Zablokuję mechanizmy śledzące, aby nie mogły Cię szpiegować. espn.com @@ -882,5 +886,6 @@ Fire Button.

Wypróbuj go! ☝️️]]>
"Udało się!" Pamiętaj: za każdym razem, gdy przeglądasz ze mną Internet, jakaś wstrętna reklama przestaje działać. 👌 + Wszystko gotowe! diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c6f9bf849f38..991b98ee9ce5 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -843,10 +843,14 @@ Iniciar a navegação Tente uma pesquisa! As tuas pesquisas no DuckDuckGo são sempre anónimas. - como dizer \"pato\" em espanhol - elenco do filme A Hora dos Campeões + como dizer \"pato\" em inglês + como dizer \"pato\" em espanhol + como dizer pato em inglês + como dizer pato em espanhol + elenco de avatar meteorologia local Surpreende-me! + receitas de jantar Experimenta visitar um site! Bloquearei rastreadores para que não te espiem. espn.com @@ -868,5 +872,6 @@ Fire Button.

Experimenta! ☝️️]]>
"Tu consegues!" Lembra-te: sempre que navegas comigo, um anúncio assustador perde as suas asas. 👌 + Já está! diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 3decd70bf6f9..4dbe6e2e6615 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -848,10 +848,14 @@ Începe navigarea Încearcă o căutare! Căutările tale DuckDuckGo sunt întotdeauna anonime. - cum se spune „rață” în spaniolă - distribuție mighty ducks + cum se spune „rață” în engleză + cum se spune „rață” în spaniolă + cum se spune rață în engleză + cum se spune rață în spaniolă + distribuția din avatar vremea locală Surprinde-mă! + rețete pentru cină Încearcă să vizitezi un site! Voi bloca instrumentele de urmărire ca să nu te mai spioneze. espn.com @@ -875,5 +879,6 @@ Fire Button.

Încearcă! ☝️️]]>
"Ai ghicit!" Reține: de fiecare dată când navighezi cu mine, o reclamă terifiantă își pierde aripile. 👌 + Gata! diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7af6a2a90f72..6ca51bd53c75 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -853,10 +853,14 @@ Начать просмотр Попробуйте поискать! Ваши поисковые запросы в DuckDuckGo всегда анонимны. - Как сказать «утка» по-испански? - Кто играет главные роли в фильме «Могучие утята»? + Как сказать «утка» по-английски? + Как сказать «утка» по-испански? + как сказать «утка» на английском? + как сказать «утка» по-испански + актерский состав аватара Местная погода Удиви меня! + рецепты на ужин Попробуйте посетить сайт! Мы заблокируем трекеры и пресечем слежку. espn.com @@ -882,5 +886,6 @@ Fire Button моментально стирает из браузера данные о посещении сайтов.

Давайте попробуем! ☝️️]]>
"Проще некуда!" Бродить по сайтам с нами — значит подрезать крылья назойливой рекламе. 👌 + Всё готово! diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 4dd0964168a4..f548450331dc 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -853,10 +853,14 @@ Začnite prehliadať Skúste vyhľadávanie! Vyhľadávania v službe DuckDuckGo sú vždy anonymné. - ako sa povie „kačica“ po španielsky - mocné kačice obsadenie + ako sa povie „kačica“ po anglicky + ako sa povie „kačica“ po španielsky + ako sa povie „kačica“ po anglicky + ako sa povie „kačica“ po španielsky + obsadenie avatara Miestne počasie Prekvapte ma! + Recepty na večeru Skúste navštíviť stránku! Zablokujem sledovacie zariadenia, ktoré by vás mohli špehovať. espn.com @@ -882,5 +886,6 @@ Fire Button.

Skúste to! ☝️️]]>
"Hotovo!" Pamätajte: zakaždým, keď prehliadate v našej aplikácii, tak čudným reklamám pristrihávate krídla. 👌 + Všetko je hotové! diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 4c299938fffc..a0a4e1c533d5 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -853,10 +853,14 @@ Začni brskanje Preizkusi z iskanjem! Vaša iskanja v DuckDuckGo so vedno anonimna. - kako se reče »raca« v španščini - zasedba mogočnih racmanov + kako se reče »raca« v angleščini + kako se reče »raca« v španščini + kako se reče raca v angleščini + kako se reče raca v španščini + igralska zasedba avatarja lokalno vreme Preseneti me! + recepti za večerjo Poskusite obiskati spletno stran! Blokiral bom sledilnike, da ne bodo vohunili za vami. espn.com @@ -882,5 +886,6 @@ Fire Button.

Poskusite! ☝️️]]>
"Uspelo vam bo!" Ne pozabite: Vedno kadar brskate z mano, shrljivemu oglasu pristrižete peruti. 👌 + Končano! diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index fcb91ba23709..57ac5a80931b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -843,10 +843,14 @@ Börja bläddra Pröva att söka! Dina DuckDuckGo-sökningar är alltid anonyma. - vad heter ”anka” på spanska - medverkande i mighty ducks + hur säger man ”anka” på engelska + vad heter ”anka” på spanska + hur säger man anka på engelska + hur säger man anka på spanska + rollbesättningen för avatar lokalt väder Överraska mig! + middagsrecept Prova att besöka en webbplats! Jag blockerar spårare så att de inte kan spionera på dig. espn.com @@ -868,5 +872,6 @@ Fire Button.

Prova! ☝️️]]>
"Du klarar det här!" Kom ihåg: varje gång du surfar med mig förlorar en läskig annons sina vingar. 👌 + Allt klart! diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 96a6314f736d..a40be6259707 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -843,10 +843,14 @@ Gezinmeye Başla Bir şeyler arayın! DuckDuckGo aramalarınız her zaman anonimdir. - ispanyolca \"ördek\" nasıl denir - mighty ducks oyuncu kadrosu + ingilizcede \"ördek\" nasıl denir + ispanyolca \"ördek\" nasıl denir + ingilizcede ördek nasıl denir + ispanyolcada ördek nasıl denir + avatar oyuncu kadrosu yerel hava durumu Şaşırt beni! + akşam yemeği tarifleri Bir siteyi ziyaret etmeyi deneyin! Sizi gözetlemelerini önlemek için izleyicileri engelleyeceğim. espn.com @@ -868,5 +872,6 @@ Fire Button ile göz atma etkinliğinizi anında temizleyin.

Hemen deneyin! ☝️️]]>
"İşte bu kadar!" Unutmayın: İnterneti benimle ne kadar çok gezerseniz rahatsız edici reklamları da o kadar az görürsünüz. 👌 + Hepsi tamam! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dfbd370c7f85..1625557c87e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -842,10 +842,14 @@ Start Browsing Try a search! Your DuckDuckGo searches are always anonymous. - how to say “duck” in spanish - mighty ducks cast + how to say “duck” in english + how to say “duck” in spanish + how to say duck in english + how to say duck in spanish + cast of avatar local weather Surprise me! + dinner recipes Try visiting a site! I\'ll block trackers so they can\'t spy on you. espn.com @@ -867,5 +871,6 @@ Fire Button.

Give it a try! ☝️️]]>
You\'ve got this!" Remember: every time you browse with me a creepy ad loses its wings. 👌 + All done! diff --git a/app/src/test/java/com/duckduckgo/app/cta/ui/CtaTest.kt b/app/src/test/java/com/duckduckgo/app/cta/ui/CtaTest.kt index 35f49ba9c451..3e984ff44cf8 100644 --- a/app/src/test/java/com/duckduckgo/app/cta/ui/CtaTest.kt +++ b/app/src/test/java/com/duckduckgo/app/cta/ui/CtaTest.kt @@ -32,7 +32,9 @@ import com.duckduckgo.app.trackerdetection.model.TrackerStatus import com.duckduckgo.app.trackerdetection.model.TrackerType import com.duckduckgo.app.trackerdetection.model.TrackingEvent import java.util.concurrent.TimeUnit -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -120,7 +122,7 @@ class CtaTest { @Test fun whenCtaIsBubbleTypeReturnCorrectCancelParameters() { - val testee = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) + val testee = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) val value = testee.pixelCancelParameters() assertEquals(1, value.size) @@ -130,7 +132,7 @@ class CtaTest { @Test fun whenCtaIsBubbleTypeReturnCorrectOkParameters() { - val testee = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) + val testee = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) val value = testee.pixelOkParameters() assertEquals(1, value.size) @@ -143,7 +145,7 @@ class CtaTest { whenever(mockOnboardingStore.onboardingDialogJourney).thenReturn(null) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis()) - val testee = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) + val testee = DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore) val expectedValue = "${testee.ctaPixelParam}:0" val value = testee.pixelShownParameters() @@ -231,7 +233,7 @@ class CtaTest { @Test fun whenCtaIsDialogTypeReturnCorrectCancelParameters() { - val testee = DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) + val testee = OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) val value = testee.pixelCancelParameters() assertEquals(1, value.size) @@ -241,7 +243,7 @@ class CtaTest { @Test fun whenCtaIsDialogTypeReturnCorrectOkParameters() { - val testee = DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) + val testee = OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) val value = testee.pixelOkParameters() assertEquals(1, value.size) @@ -253,7 +255,7 @@ class CtaTest { fun whenCtaIsDialogTypeReturnCorrectShownParameters() { whenever(mockOnboardingStore.onboardingDialogJourney).thenReturn(null) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis()) - val testee = DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) + val testee = OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) val expectedValue = "${testee.ctaPixelParam}:0" val value = testee.pixelShownParameters() @@ -267,7 +269,7 @@ class CtaTest { val existingJourney = "s:0-t:1" whenever(mockOnboardingStore.onboardingDialogJourney).thenReturn(existingJourney) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - val testee = DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) + val testee = OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore) val expectedValue = "$existingJourney-${testee.ctaPixelParam}:1" val value = testee.pixelShownParameters() @@ -282,8 +284,8 @@ class CtaTest { TestingEntity("Amazon", "Amazon", 9.0), ) - val testee = DaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, trackers, "http://www.trackers.com") - val value = testee.getDaxText(mockActivity) + val testee = OnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, trackers) + val value = testee.getTrackersDescription(mockActivity, trackers) assertEquals("Facebook, OtherwithMultiple", value) } @@ -295,8 +297,8 @@ class CtaTest { TestingEntity("Other", "Other", 9.0), ) - val testee = DaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, trackers, "http://www.trackers.com") - val value = testee.getDaxText(mockActivity) + val testee = OnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, trackers) + val value = testee.getTrackersDescription(mockActivity, trackers) assertEquals("Facebook, OtherwithZero", value) } @@ -326,13 +328,12 @@ class CtaTest { val site = site(events = trackers) val testee = - DaxDialogCta.DaxTrackersBlockedCta( + OnboardingDaxDialogCta.DaxTrackersBlockedCta( mockOnboardingStore, mockAppInstallStore, site.orderedTrackerBlockedEntities(), - "http://www.trackers.com", ) - val value = testee.getDaxText(mockActivity) + val value = testee.getTrackersDescription(mockActivity, site.orderedTrackerBlockedEntities()) assertEquals("Other, FacebookwithZero", value) } @@ -361,13 +362,12 @@ class CtaTest { ) val site = site(events = trackers) - val testee = DaxDialogCta.DaxTrackersBlockedCta( + val testee = OnboardingDaxDialogCta.DaxTrackersBlockedCta( mockOnboardingStore, mockAppInstallStore, site.orderedTrackerBlockedEntities(), - "http://www.trackers.com", ) - val value = testee.getDaxText(mockActivity) + val value = testee.getTrackersDescription(mockActivity, site.orderedTrackerBlockedEntities()) assertEquals("FacebookwithZero", value) } @@ -396,13 +396,12 @@ class CtaTest { ) val site = site(events = trackers) - val testee = DaxDialogCta.DaxTrackersBlockedCta( + val testee = OnboardingDaxDialogCta.DaxTrackersBlockedCta( mockOnboardingStore, mockAppInstallStore, site.orderedTrackerBlockedEntities(), - "http://www.trackers.com", ) - val value = testee.getDaxText(mockActivity) + val value = testee.getTrackersDescription(mockActivity, site.orderedTrackerBlockedEntities()) assertEquals("OtherwithZero", value) } @@ -415,8 +414,8 @@ class CtaTest { TestingEntity("Facebook", "Facebook", 9.0), ) - val testee = DaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, trackers, "http://www.trackers.com") - val value = testee.getDaxText(mockActivity) + val testee = OnboardingDaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, trackers) + val value = testee.getTrackersDescription(mockActivity, trackers) assertEquals("FacebookwithZero", value) } @@ -426,7 +425,7 @@ class CtaTest { val existingJourney = "s:0-t:1" whenever(mockOnboardingStore.onboardingDialogJourney).thenReturn(existingJourney) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - val testee = DaxFireDialogCta.TryClearDataCta(mockOnboardingStore, mockAppInstallStore) + val testee = OnboardingDaxDialogCta.DaxFireButtonCta(mockOnboardingStore, mockAppInstallStore) val expectedValue = "$existingJourney-${testee.ctaPixelParam}:1" val value = testee.pixelShownParameters() diff --git a/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt index 595361186e68..fcc9c1bb38f4 100644 --- a/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt @@ -22,7 +22,6 @@ import com.duckduckgo.app.launch.LaunchViewModel.Command.Home import com.duckduckgo.app.launch.LaunchViewModel.Command.Onboarding import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager import com.duckduckgo.app.referral.StubAppReferrerFoundStateListener import com.duckduckgo.common.test.CoroutineTestRule import kotlinx.coroutines.test.runTest @@ -45,7 +44,6 @@ class LaunchViewModelTest { private val userStageStore = mock() private val mockCommandObserver: Observer = mock() - private val mockExtendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager = mock() private lateinit var testee: LaunchViewModel @@ -59,7 +57,6 @@ class LaunchViewModelTest { testee = LaunchViewModel( userStageStore, StubAppReferrerFoundStateListener("xx"), - mockExtendedOnboardingExperimentVariantManager, ) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) testee.command.observeForever(mockCommandObserver) @@ -74,7 +71,6 @@ class LaunchViewModelTest { testee = LaunchViewModel( userStageStore, StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000), - mockExtendedOnboardingExperimentVariantManager, ) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) testee.command.observeForever(mockCommandObserver) @@ -89,7 +85,6 @@ class LaunchViewModelTest { testee = LaunchViewModel( userStageStore, StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE), - mockExtendedOnboardingExperimentVariantManager, ) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) testee.command.observeForever(mockCommandObserver) @@ -101,7 +96,7 @@ class LaunchViewModelTest { @Test fun whenOnboardingShouldNotShowAndReferrerDataReturnsQuicklyThenCommandIsHome() = runTest { - testee = LaunchViewModel(userStageStore, StubAppReferrerFoundStateListener("xx"), mockExtendedOnboardingExperimentVariantManager) + testee = LaunchViewModel(userStageStore, StubAppReferrerFoundStateListener("xx")) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) testee.command.observeForever(mockCommandObserver) testee.determineViewToShow() @@ -113,7 +108,6 @@ class LaunchViewModelTest { testee = LaunchViewModel( userStageStore, StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000), - mockExtendedOnboardingExperimentVariantManager, ) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) testee.command.observeForever(mockCommandObserver) @@ -126,7 +120,6 @@ class LaunchViewModelTest { testee = LaunchViewModel( userStageStore, StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE), - mockExtendedOnboardingExperimentVariantManager, ) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) testee.command.observeForever(mockCommandObserver) diff --git a/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerPageCountTest.kt b/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerPageCountTest.kt index fed05b13d554..5c24c75310ed 100644 --- a/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerPageCountTest.kt +++ b/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerPageCountTest.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.onboarding.ui import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.global.DefaultRoleBrowserDialog -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -34,7 +33,6 @@ class OnboardingPageManagerPageCountTest(private val testCase: TestCase) { private val onboardingPageBuilder: OnboardingPageBuilder = mock() private val mockDefaultBrowserDetector: DefaultBrowserDetector = mock() private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog = mock() - private val mockExtendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager = mock() @Before fun setup() { @@ -42,7 +40,6 @@ class OnboardingPageManagerPageCountTest(private val testCase: TestCase) { defaultRoleBrowserDialog, onboardingPageBuilder, mockDefaultBrowserDetector, - mockExtendedOnboardingExperimentVariantManager, ) } diff --git a/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt b/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt index f0e497f1f418..c7df015bd0ad 100644 --- a/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt +++ b/app/src/test/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.onboarding.ui import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.global.DefaultRoleBrowserDialog -import com.duckduckgo.app.onboarding.ui.page.experiment.ExtendedOnboardingExperimentVariantManager import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -31,16 +30,13 @@ class OnboardingPageManagerTest { private val onboardingPageBuilder: OnboardingPageBuilder = mock() private val mockDefaultBrowserDetector: DefaultBrowserDetector = mock() private val defaultRoleBrowserDialog: DefaultRoleBrowserDialog = mock() - private val mockExtendedOnboardingExperimentVariantManager: ExtendedOnboardingExperimentVariantManager = mock() @Before fun setup() { - whenever(mockExtendedOnboardingExperimentVariantManager.isComparisonChartEnabled()).thenReturn(false) testee = OnboardingPageManagerWithTrackerBlocking( defaultRoleBrowserDialog, onboardingPageBuilder, mockDefaultBrowserDetector, - mockExtendedOnboardingExperimentVariantManager, ) } diff --git a/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt index 5e2f08c9067f..1ad68876a795 100644 --- a/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,136 +16,172 @@ package com.duckduckgo.app.onboarding.ui.page +import android.content.Context import android.content.Intent -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.onboarding.ui.page.WelcomePage.Companion.PreOnboardingDialogType +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowComparisonChart +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowDefaultBrowserDialog +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowSuccessDialog +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.OnboardingExperimentPixel import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.ObsoleteCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertTrue -import org.junit.Before +import org.junit.Assert import org.junit.Rule import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -@ObsoleteCoroutinesApi -@ExperimentalTime class WelcomePageViewModelTest { - @get:Rule - var instantTaskExecutorRule = InstantTaskExecutorRule() - @get:Rule @Suppress("unused") val coroutineRule = CoroutineTestRule() - @Mock - private lateinit var pixel: Pixel + private val mockDefaultRoleBrowserDialog: DefaultRoleBrowserDialog = mock() + private val mockContext: Context = mock() + private val mockPixel: Pixel = mock() + private val mockAppInstallStore: AppInstallStore = mock() + + private val testee: WelcomePageViewModel by lazy { + WelcomePageViewModel( + mockDefaultRoleBrowserDialog, + mockContext, + mockPixel, + mockAppInstallStore, + ) + } + + @Test + fun whenInitialDialogIsShownThenSendPixel() { + testee.onDialogShown(PreOnboardingDialogType.INITIAL) - @Mock - private lateinit var appInstallStore: AppInstallStore + verify(mockPixel).fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_INTRO_SHOWN) + } - @Mock - private lateinit var defaultRoleBrowserDialog: DefaultRoleBrowserDialog + @Test + fun whenComparisonChartDialogIsShownThenSendPixel() { + testee.onDialogShown(PreOnboardingDialogType.COMPARISON_CHART) - private val events = MutableSharedFlow(replay = 1) + verify(mockPixel).fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_COMPARISON_CHART_SHOWN) + } - private lateinit var viewModel: WelcomePageViewModel + @Test + fun whenAffirmationDialogIsShownThenSendPixel() { + testee.onDialogShown(PreOnboardingDialogType.CELEBRATION) - private lateinit var viewEvents: Flow + verify(mockPixel).fire(OnboardingExperimentPixel.PixelName.PREONBOARDING_AFFIRMATION_SHOWN) + } - @Before - fun setup() { - MockitoAnnotations.openMocks(this) - viewModel = WelcomePageViewModel( - appInstallStore = appInstallStore, - context = mock(), - pixel = pixel, - defaultRoleBrowserDialog = defaultRoleBrowserDialog, - ) + @Test + fun whenNotificationsRuntimePermissionsAreRequestedSendPixel() { + testee.notificationRuntimePermissionRequested() - viewEvents = events.flatMapLatest { viewModel.reduce(it) } + verify(mockPixel).fire(OnboardingExperimentPixel.PixelName.NOTIFICATION_RUNTIME_PERMISSION_SHOWN) } @Test - fun whenOnPrimaryCtaClickedAndShouldNotShowDialogThenFireAndFinish() = runTest { - whenever(defaultRoleBrowserDialog.shouldShowDialog()).thenReturn(false) + fun whenNotificationsRuntimePermissionsAreGrantedThenSendPixel() { + testee.notificationRuntimePermissionGranted() - events.emit(WelcomePageView.Event.OnPrimaryCtaClicked) + verify(mockPixel).fire( + AppPixelName.NOTIFICATIONS_ENABLED, + mapOf(OnboardingExperimentPixel.PixelParameter.FROM_ONBOARDING to true.toString()), + ) + } - viewEvents.test { - assertTrue(awaitItem() == WelcomePageView.State.Finish) + @Test + fun givenInitialDialogWhenOnPrimaryCtaClickedThenShowComparisonChart() = runTest { + testee.onPrimaryCtaClicked(PreOnboardingDialogType.INITIAL) + + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is ShowComparisonChart) } } @Test - fun whenOnPrimaryCtaClickedAndShouldShowDialogAndShowThenFireAndEmitShowDialog() = runTest { - whenever(defaultRoleBrowserDialog.shouldShowDialog()).thenReturn(true) - val intent = Intent() - whenever(defaultRoleBrowserDialog.createIntent(any())).thenReturn(intent) + fun givenComparisonChartDialogWhenOnPrimaryCtaClickedThenSendPixel() { + whenever(mockDefaultRoleBrowserDialog.shouldShowDialog()).thenReturn(true) + testee.onPrimaryCtaClicked(PreOnboardingDialogType.COMPARISON_CHART) - events.emit(WelcomePageView.Event.OnPrimaryCtaClicked) + verify(mockPixel).fire( + OnboardingExperimentPixel.PixelName.PREONBOARDING_CHOOSE_BROWSER_PRESSED, + mapOf(OnboardingExperimentPixel.PixelParameter.DEFAULT_BROWSER to "false"), + ) + } - viewEvents.test { - assertTrue(awaitItem() == WelcomePageView.State.ShowDefaultBrowserDialog(intent)) + @Test + fun whenChooseBrowserClickedIfDDGNotSetAsDefaultThenShowChooseBrowserDialog() = runTest { + val mockIntent: Intent = mock() + whenever(mockDefaultRoleBrowserDialog.createIntent(mockContext)).thenReturn(mockIntent) + whenever(mockDefaultRoleBrowserDialog.shouldShowDialog()).thenReturn(true) + testee.onPrimaryCtaClicked(PreOnboardingDialogType.COMPARISON_CHART) + + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is ShowDefaultBrowserDialog) } } @Test - fun whenOnPrimaryCtaClickedAndShouldShowDialogNullIntentThenFireAndFinish() = runTest { - whenever(defaultRoleBrowserDialog.shouldShowDialog()).thenReturn(true) - whenever(defaultRoleBrowserDialog.createIntent(any())).thenReturn(null) + fun whenChooseBrowserClickedIfDDGSetAsDefaultThenFinishFlow() = runTest { + whenever(mockDefaultRoleBrowserDialog.shouldShowDialog()).thenReturn(false) + testee.onPrimaryCtaClicked(PreOnboardingDialogType.COMPARISON_CHART) - events.emit(WelcomePageView.Event.OnPrimaryCtaClicked) - - viewEvents.test { - assertTrue(awaitItem() == WelcomePageView.State.Finish) - verify(pixel).fire(AppPixelName.DEFAULT_BROWSER_DIALOG_NOT_SHOWN) + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is Finish) } } @Test - fun whenOnDefaultBrowserSetThenCallDialogShownFireAndFinish() = runTest { - events.emit(WelcomePageView.Event.OnDefaultBrowserSet) - - viewEvents.test { - assertTrue(awaitItem() == WelcomePageView.State.Finish) - verify(defaultRoleBrowserDialog).dialogShown() - verify(pixel).fire( - AppPixelName.DEFAULT_BROWSER_SET, - mapOf( - Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(), - ), - ) + fun whenDDGIsNOTSetAsDefaultBrowserFromSystemDialogThenSetPreferenceAndSendPixel() { + testee.onDefaultBrowserNotSet() + + verify(mockDefaultRoleBrowserDialog).dialogShown() + verify(mockAppInstallStore).defaultBrowser = false + verify(mockPixel).fire( + AppPixelName.DEFAULT_BROWSER_NOT_SET, + mapOf(Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString()), + ) + } + + @Test + fun whenDDGIsSetAsDefaultBrowserFromSystemDialogThenSetPreferenceAndSendPixel() { + testee.onDefaultBrowserSet() + + verify(mockDefaultRoleBrowserDialog).dialogShown() + verify(mockAppInstallStore).defaultBrowser = true + verify(mockPixel).fire( + AppPixelName.DEFAULT_BROWSER_SET, + mapOf(Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString()), + ) + } + + @Test + fun whenDDGIsSetAsDefaultBrowserFromOnboardingThenShowCelebrationScreen() = runTest { + testee.onDefaultBrowserSet() + + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is ShowSuccessDialog) } } @Test - fun whenOnDefaultBrowserNotSetThenCallDialogShownFireAndFinish() = runTest { - events.emit(WelcomePageView.Event.OnDefaultBrowserNotSet) - - viewEvents.test { - assertTrue(awaitItem() == WelcomePageView.State.Finish) - verify(defaultRoleBrowserDialog).dialogShown() - verify(pixel).fire( - AppPixelName.DEFAULT_BROWSER_NOT_SET, - mapOf( - Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(), - ), - ) + fun givenCelebrationDialogWhenOnPrimaryCtaClickedThenFinishFlow() = runTest { + testee.onPrimaryCtaClicked(PreOnboardingDialogType.CELEBRATION) + + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is Finish) } } } diff --git a/app/version/release-notes b/app/version/release-notes index a43a0bfbca62..4185fb90429d 100644 --- a/app/version/release-notes +++ b/app/version/release-notes @@ -1 +1,2 @@ -Bug fixes and other improvements \ No newline at end of file +With Sync & Backup enabled, you'll now see more helpful confirmation details when deleting passwords. +As usual we also included some bug fixes and improvements. \ No newline at end of file diff --git a/app/version/version.properties b/app/version/version.properties index ea0a9ebd32f4..2315d7d57c45 100644 --- a/app/version/version.properties +++ b/app/version/version.properties @@ -1 +1 @@ -VERSION=5.202.0 \ No newline at end of file +VERSION=5.203.0 \ No newline at end of file diff --git a/autoconsent/autoconsent-impl/libs/autoconsent-bundle.js b/autoconsent/autoconsent-impl/libs/autoconsent-bundle.js index 6e5da860dc44..7a4cd14c40d1 100644 --- a/autoconsent/autoconsent-impl/libs/autoconsent-bundle.js +++ b/autoconsent/autoconsent-impl/libs/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,n=null,o=!1){let i=null;return i=null!=n?Array.from(n.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 n=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const o of t.textFilter)if(-1!==n.indexOf(o.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==n.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const n=window.getComputedStyle(e);let o=!0;for(const e of t.styleFilters){const t=n[e.option];o=e.negated?o&&t!==e.value:o&&t===e.value}return o}))),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((n=>{const o=e.base;e.setBase(n);const i=e.find(t.childFilter);return e.setBase(o),null!=i.target}))),o?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,n),i[0])}static find(t,n=!1){const o=[];if(null!=t.parent){const i=e.findElement(t.parent,null,n);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const s=e.findElement(t.target,i,n);s instanceof Array?s.forEach((e=>{o.push({parent:i,target:e})})):o.push({parent:i,target:s})})),o;{const s=e.findElement(t.target,i,n);s instanceof Array?s.forEach((e=>{o.push({parent:i,target:e})})):o.push({parent:i,target:s})}}}else{const i=e.findElement(t.target,null,n);i instanceof Array?i.forEach((e=>{o.push({parent:null,target:e})})):o.push({parent:null,target:i})}return 0===o.length&&o.push({parent:null,target:null}),n?o:(1!==o.length&&console.warn("Multiple results found, even though multiple false",o),o[0])}};e.base=null;var t=e;function n(e){const n=t.find(e);return"css"===e.type?!!n.target:"checkbox"===e.type?!!n.target&&n.target.checked:void 0}async function o(e,c){switch(e.type){case"click":return async function(e){const n=t.find(e);null!=n.target&&n.target.click();return s(i)}(e);case"list":return async function(e,t){for(const n of e.actions)await o(n,t)}(e,c);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){n(i.matcher)!==e&&await o(i.toggleAction)}else e?await o(i.trueAction):await o(i.falseAction)}}(e,c);case"ifcss":return async function(e,n){const i=t.find(e);i.target?e.falseAction&&await o(e.falseAction,n):e.trueAction&&await o(e.trueAction,n)}(e,c);case"waitcss":return async function(e){await new Promise((n=>{let o=e.retries||10;const i=e.waitTime||250,s=()=>{const c=t.find(e);(e.negated&&c.target||!e.negated&&!c.target)&&o>0?(o-=1,setTimeout(s,i)):n()};s()}))}(e);case"foreach":return async function(e,n){const i=t.find(e,!0),s=t.base;for(const s of i)s.target&&(t.setBase(s.target),await o(e.action,n));t.setBase(s)}(e,c);case"hide":return async function(e){const n=t.find(e);n.target&&n.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const n=t.find(e),o=t.find(e.dragTarget);if(n.target){const e=n.target.getBoundingClientRect(),t=o.target.getBoundingClientRect();let i=t.top-e.top,s=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(s=0),"x"===this.config.axis.toLowerCase()&&(i=0);const c=window.screenX+e.left+e.width/2,r=window.screenY+e.top+e.height/2,a=e.left+e.width/2,l=e.top+e.height/2,u=document.createEvent("MouseEvents");u.initMouseEvent("mousedown",!0,!0,window,0,c,r,a,l,!1,!1,!1,!1,0,n.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,c+s,r+i,a+s,l+i,!1,!1,!1,!1,0,n.target);const h=document.createEvent("MouseEvents");h.initMouseEvent("mouseup",!0,!0,window,0,c+s,r+i,a+s,l+i,!1,!1,!1,!1,0,n.target),n.target.dispatchEvent(u),await this.waitTimeout(10),n.target.dispatchEvent(d),await this.waitTimeout(10),n.target.dispatchEvent(h)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await s(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(n){console.warn("eval error",n,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}var i=0;function s(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function c(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var r=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)}},a={pending:new Map,sendContentMessage:null};var l={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,n=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(n).filter((e=>t.includes(e))).every((e=>!1===n[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},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_TRUSTARC_FRAME_TEST:()=>window&&window.QueryString&&"0"===window.QueryString.preferences,EVAL_TRUSTARC_FRAME_GTM:()=>window&&window.QueryString&&"1"===window.QueryString.gtm,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||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!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_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 u={main:!0,frame:!1,urlPattern:""},d=class{constructor(e){this.runContext=u,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=l[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const n=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){n.evals&&console.log("inline eval:",e,t);let o=!1;try{o=!!t.call(globalThis)}catch(t){n.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(o)}const o=`(${t.toString()})()`;return n.evals&&console.log("async eval:",e,o),function(e,t){const n=c();a.sendContentMessage({type:"eval",id:n,code:e,snippetId:t});const o=new r(n);return a.pending.set(o.id,o),o.promise}(o,e).catch((t=>(n.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...u,...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,n){return this.autoconsent.domActions.waitForVisible(e,t,n)}waitForThenClick(e,t,n){return this.autoconsent.domActions.waitForThenClick(e,t,n)}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)}},h=class extends d{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||u}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=[],n=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 n=this.mainWorldEval(e.eval);t.push(n)}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 o=await this.evaluateRuleStep(e.if);n.rulesteps&&console.log("Condition is",o),o?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 n.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 n of e){t.rulesteps&&console.log("Running rule...",n);const e=await this.evaluateRuleStep(n);if(t.rulesteps&&console.log("...rule result",e),!e&&!n.optional)return!1}return!0}},p=class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=u,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=>n(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>n(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||o(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}};function m(e="autoconsent-css-rules"){const t=`style#${e}`,n=document.querySelector(t);if(n&&n instanceof HTMLStyleElement)return n;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,n=document.createElement("style");return n.id=e,t.appendChild(n),n}}function _(e,t,n="display"){const o=`${t} { ${"opacity"===n?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=o,t.length>0)}async function f(e,t,n){const o=await e();return!o&&t>0?new Promise((o=>{setTimeout((async()=>{o(f(e,t-1,n))}),n)})):Promise.resolve(o)}function E(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 g(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}},n=(o=t,globalThis.structuredClone?structuredClone(o):JSON.parse(JSON.stringify(o)));var o;for(const o of Object.keys(t))void 0!==e[o]&&(n[o]=e[o]);return n}var w="#truste-show-consent",A="#truste-consent-track",y=[class extends d{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${A}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!0}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${w},${A}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${A}`,"all")}openFrame(){this.click(w)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(_(m(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${A}`),this.click(w),setTimeout((()=>{m().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends d{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!0}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 f((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 f((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){if(await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST"))return!0;let e=3e3;return await this.mainWorldEval("EVAL_TRUSTARC_FRAME_GTM")&&(e=1500),await f((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",e),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',e),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",e),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",e),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",10*e),!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}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST")}},class extends d{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 d{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:!0,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 f((()=>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",n=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===n)return await this.wait(1500),this.click(e);1===n?this.click(t):2===n&&(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 d{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 d{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(),"#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 d{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 f((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends d{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!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!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 d{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 n of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(n))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 d{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 f((()=>!!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 d{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 E(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 d{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 f((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends d{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await f((()=>{const e=document.querySelector("#cmp-app-container iframe");return!!e.contentDocument?.querySelector(".cmp__dialog input")}),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}],C=class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const n=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,n),n.length>0&&(t?n.forEach((e=>e.click())):n[0].click()),n.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const n=this.elementSelector(e),o=new Array(n.length);return n.forEach(((e,t)=>{o[t]=E(e)})),"none"===t?o.every((e=>!e)):0!==o.length&&("any"===t?o.some((e=>e)):o.every((e=>e)))}waitForElement(e,t=1e4){const n=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),f((()=>this.elementSelector(e).length>0),n,200)}waitForVisible(e,t=1e4,n="any"){return f((()=>this.elementVisible(e,n)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,n=!1){return await this.waitForElement(e,t),this.click(e,n)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return _(m(),e,t)}prehide(e){const t=m("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),_(t,e,"opacity")}undoPrehide(){const e=m("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 n=e.slice(6),o=document.evaluate(n,t,null,XPathResult.ANY_TYPE,null);let i=null;const s=[];for(;i=o.iterateNext();)s.push(i);return s}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,n=document;for(const o of e){if(t=this.querySingleReplySelector(o,n),0===t.length)return[];n=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}};const k=new class{constructor(e,t=null,n=null){if(this.id=c(),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,n);else{n&&this.parseDeclarativeRules(n);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new C(this)}initialize(e,t){const n=g(e);if(n.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=n,n.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,n),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 n.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 h(e,this))}addConsentomaticCMP(e,t){this.rules.push(new p(`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 n=[],o=[];for(const e of t)e.isCosmetic?o.push(e):n.push(e);let i=!1,s=await this.detectPopups(n,(async e=>{i=await this.handlePopup(e)}));if(0===s.length&&(s=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}))),0===s.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(s.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:s.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 n=[];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}),n.push(e))}catch(n){t.errors&&console.warn(`error detecting ${e.name}`,n)}return 0===n.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):n}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 n=e.map((e=>this.detectPopup(e)));await Promise.any(n).then((e=>{t(e)})).catch((()=>null));const o=await Promise.allSettled(n),i=[];for(const e of o)"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,n=500){const o=this.config.logs;o.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(o.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(n),this.waitForPopup(e,t-1,n)):(o.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#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 n=a.pending.get(e);n?(a.pending.delete(e),n.timer&&window.clearTimeout(n.timer),n.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{AutoconsentAndroid.process(JSON.stringify(e))}));window.autoconsentMessageCallback=e=>{k.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,n=null,o=!1){let i=null;return i=null!=n?Array.from(n.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 n=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const o of t.textFilter)if(-1!==n.indexOf(o.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==n.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const n=window.getComputedStyle(e);let o=!0;for(const e of t.styleFilters){const t=n[e.option];o=e.negated?o&&t!==e.value:o&&t===e.value}return o}))),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((n=>{const o=e.base;e.setBase(n);const i=e.find(t.childFilter);return e.setBase(o),null!=i.target}))),o?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,n),i[0])}static find(t,n=!1){const o=[];if(null!=t.parent){const i=e.findElement(t.parent,null,n);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const s=e.findElement(t.target,i,n);s instanceof Array?s.forEach((e=>{o.push({parent:i,target:e})})):o.push({parent:i,target:s})})),o;{const s=e.findElement(t.target,i,n);s instanceof Array?s.forEach((e=>{o.push({parent:i,target:e})})):o.push({parent:i,target:s})}}}else{const i=e.findElement(t.target,null,n);i instanceof Array?i.forEach((e=>{o.push({parent:null,target:e})})):o.push({parent:null,target:i})}return 0===o.length&&o.push({parent:null,target:null}),n?o:(1!==o.length&&console.warn("Multiple results found, even though multiple false",o),o[0])}};e.base=null;var t=e;function n(e){const n=t.find(e);return"css"===e.type?!!n.target:"checkbox"===e.type?!!n.target&&n.target.checked:void 0}async function o(e,c){switch(e.type){case"click":return async function(e){const n=t.find(e);null!=n.target&&n.target.click();return s(i)}(e);case"list":return async function(e,t){for(const n of e.actions)await o(n,t)}(e,c);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){n(i.matcher)!==e&&await o(i.toggleAction)}else e?await o(i.trueAction):await o(i.falseAction)}}(e,c);case"ifcss":return async function(e,n){const i=t.find(e);i.target?e.falseAction&&await o(e.falseAction,n):e.trueAction&&await o(e.trueAction,n)}(e,c);case"waitcss":return async function(e){await new Promise((n=>{let o=e.retries||10;const i=e.waitTime||250,s=()=>{const c=t.find(e);(e.negated&&c.target||!e.negated&&!c.target)&&o>0?(o-=1,setTimeout(s,i)):n()};s()}))}(e);case"foreach":return async function(e,n){const i=t.find(e,!0),s=t.base;for(const s of i)s.target&&(t.setBase(s.target),await o(e.action,n));t.setBase(s)}(e,c);case"hide":return async function(e){const n=t.find(e);n.target&&n.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const n=t.find(e),o=t.find(e.dragTarget);if(n.target){const e=n.target.getBoundingClientRect(),t=o.target.getBoundingClientRect();let i=t.top-e.top,s=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(s=0),"x"===this.config.axis.toLowerCase()&&(i=0);const c=window.screenX+e.left+e.width/2,r=window.screenY+e.top+e.height/2,a=e.left+e.width/2,l=e.top+e.height/2,u=document.createEvent("MouseEvents");u.initMouseEvent("mousedown",!0,!0,window,0,c,r,a,l,!1,!1,!1,!1,0,n.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,c+s,r+i,a+s,l+i,!1,!1,!1,!1,0,n.target);const h=document.createEvent("MouseEvents");h.initMouseEvent("mouseup",!0,!0,window,0,c+s,r+i,a+s,l+i,!1,!1,!1,!1,0,n.target),n.target.dispatchEvent(u),await this.waitTimeout(10),n.target.dispatchEvent(d),await this.waitTimeout(10),n.target.dispatchEvent(h)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await s(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(n){console.warn("eval error",n,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}var i=0;function s(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function c(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var r=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)}},a={pending:new Map,sendContentMessage:null};var l={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,n=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(n).filter((e=>t.includes(e))).every((e=>!1===n[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},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_TRUSTARC_FRAME_TEST:()=>window&&window.QueryString&&"0"===window.QueryString.preferences,EVAL_TRUSTARC_FRAME_GTM:()=>window&&window.QueryString&&"1"===window.QueryString.gtm,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_FIDES_DETECT_POPUP:()=>window.Fides?.initialized,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_KETCH_TEST:()=>document.cookie.includes("_ketch_consent_v1_"),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||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!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_ROBLOX_TEST:()=>document.cookie.includes("RBXcb"),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 u={main:!0,frame:!1,urlPattern:""},d=class{constructor(e){this.runContext=u,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=l[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const n=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){n.evals&&console.log("inline eval:",e,t);let o=!1;try{o=!!t.call(globalThis)}catch(t){n.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(o)}const o=`(${t.toString()})()`;return n.evals&&console.log("async eval:",e,o),function(e,t){const n=c();a.sendContentMessage({type:"eval",id:n,code:e,snippetId:t});const o=new r(n);return a.pending.set(o.id,o),o.promise}(o,e).catch((t=>(n.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...u,...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,n){return this.autoconsent.domActions.waitForVisible(e,t,n)}waitForThenClick(e,t,n){return this.autoconsent.domActions.waitForThenClick(e,t,n)}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)}},h=class extends d{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||u}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=[],n=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 n=this.mainWorldEval(e.eval);t.push(n)}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 o=await this.evaluateRuleStep(e.if);n.rulesteps&&console.log("Condition is",o),o?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 n.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 n of e){t.rulesteps&&console.log("Running rule...",n);const e=await this.evaluateRuleStep(n);if(t.rulesteps&&console.log("...rule result",e),!e&&!n.optional)return!1}return!0}},p=class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=u,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=>n(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>n(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||o(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}};function m(e="autoconsent-css-rules"){const t=`style#${e}`,n=document.querySelector(t);if(n&&n instanceof HTMLStyleElement)return n;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,n=document.createElement("style");return n.id=e,t.appendChild(n),n}}function _(e,t,n="display"){const o=`${t} { ${"opacity"===n?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=o,t.length>0)}async function E(e,t,n){const o=await e();return!o&&t>0?new Promise((o=>{setTimeout((async()=>{o(E(e,t-1,n))}),n)})):Promise.resolve(o)}function f(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 g(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}},n=(o=t,globalThis.structuredClone?structuredClone(o):JSON.parse(JSON.stringify(o)));var o;for(const o of Object.keys(t))void 0!==e[o]&&(n[o]=e[o]);return n}var w="#truste-show-consent",A="#truste-consent-track",C=[class extends d{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${A}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!0}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${w},${A}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${A}`,"all")}openFrame(){this.click(w)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(_(m(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${A}`),this.click(w),setTimeout((()=>{m().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends d{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!0}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 E((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 E((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){if(await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST"))return!0;let e=3e3;return await this.mainWorldEval("EVAL_TRUSTARC_FRAME_GTM")&&(e=1500),await E((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",e),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',e),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",e),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",e),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",10*e),!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}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST")}},class extends d{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 d{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:!0,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 E((()=>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",n=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===n)return await this.wait(1500),this.click(e);1===n?this.click(t):2===n&&(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 d{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 d{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(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background,#_evidon-background"),await this.waitForThenClick("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),await this.wait(500),await this.waitForThenClick("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends d{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 E((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends d{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!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!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 d{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 n of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(n))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 d{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 E((()=>!!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 d{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 f(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 d{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 E((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends d{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await E((()=>{const e=document.querySelector("#cmp-app-container iframe");return!!e.contentDocument?.querySelector(".cmp__dialog input")}),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}],y=class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const n=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,n),n.length>0&&(t?n.forEach((e=>e.click())):n[0].click()),n.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const n=this.elementSelector(e),o=new Array(n.length);return n.forEach(((e,t)=>{o[t]=f(e)})),"none"===t?o.every((e=>!e)):0!==o.length&&("any"===t?o.some((e=>e)):o.every((e=>e)))}waitForElement(e,t=1e4){const n=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),E((()=>this.elementSelector(e).length>0),n,200)}waitForVisible(e,t=1e4,n="any"){return E((()=>this.elementVisible(e,n)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,n=!1){return await this.waitForElement(e,t),this.click(e,n)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return _(m(),e,t)}prehide(e){const t=m("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),_(t,e,"opacity")}undoPrehide(){const e=m("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 n=e.slice(6),o=document.evaluate(n,t,null,XPathResult.ANY_TYPE,null);let i=null;const s=[];for(;i=o.iterateNext();)s.push(i);return s}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,n=document;for(const o of e){if(t=this.querySingleReplySelector(o,n),0===t.length)return[];n=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}};const k=new class{constructor(e,t=null,n=null){if(this.id=c(),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,n);else{n&&this.parseDeclarativeRules(n);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new y(this)}initialize(e,t){const n=g(e);if(n.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=n,n.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,n),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 n.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){C.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 h(e,this))}addConsentomaticCMP(e,t){this.rules.push(new p(`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 n=[],o=[];for(const e of t)e.isCosmetic?o.push(e):n.push(e);let i=!1,s=await this.detectPopups(n,(async e=>{i=await this.handlePopup(e)}));if(0===s.length&&(s=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}))),0===s.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(s.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:s.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 n=[];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}),n.push(e))}catch(n){t.errors&&console.warn(`error detecting ${e.name}`,n)}return 0===n.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):n}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 n=e.map((e=>this.detectPopup(e)));await Promise.any(n).then((e=>{t(e)})).catch((()=>null));const o=await Promise.allSettled(n),i=[];for(const e of o)"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,n=500){const o=this.config.logs;o.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(o.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(n),this.waitForPopup(e,t-1,n)):(o.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#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 n=a.pending.get(e);n?(a.pending.delete(e),n.timer&&window.clearTimeout(n.timer),n.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{AutoconsentAndroid.process(JSON.stringify(e))}));window.autoconsentMessageCallback=e=>{k.receiveMessageCallback(e)}}(); diff --git a/autoconsent/autoconsent-impl/libs/rules.json b/autoconsent/autoconsent-impl/libs/rules.json index 078861f46dc2..29a15061c63f 100644 --- a/autoconsent/autoconsent-impl/libs/rules.json +++ b/autoconsent/autoconsent-impl/libs/rules.json @@ -374,7 +374,6 @@ }, { "name": "aquasana.com", - "cosmetic": true, "prehideSelectors": [ "#consent-tracking" ], @@ -390,12 +389,24 @@ ], "optIn": [ { - "click": "#accept_consent" + "waitForThenClick": "#consent-tracking .affirm.btn" } ], "optOut": [ { - "hide": "#consent-tracking" + "if": { + "exists": "#consent-tracking .decline.btn" + }, + "then": [ + { + "click": "#consent-tracking .decline.btn" + } + ], + "else": [ + { + "hide": "#consent-tracking" + } + ] } ] }, @@ -1571,7 +1582,20 @@ ], "else": [ { - "hide": "[aria-describedby=\"cookieconsent:desc\"]" + "if": { + "exists": ".cmp-pref-link" + }, + "then": [ + { + "click": ".cmp-pref-link" + }, + { + "waitForThenClick": ".cmp-body [id*=rejectAll]" + }, + { + "waitForThenClick": ".cmp-body .cmp-save-btn" + } + ] } ] } @@ -1965,6 +1989,51 @@ } ] }, + { + "name": "cookiecuttr", + "vendorUrl": "https://github.com/cdwharton/cookieCuttr", + "cosmetic": false, + "runContext": { + "main": true, + "frame": false, + "urlPattern": "" + }, + "prehideSelectors": [ + ".cc-cookies" + ], + "detectCmp": [ + { + "exists": ".cc-cookies .cc-cookie-accept" + } + ], + "detectPopup": [ + { + "visible": ".cc-cookies .cc-cookie-accept" + } + ], + "optIn": [ + { + "waitForThenClick": ".cc-cookies .cc-cookie-accept" + } + ], + "optOut": [ + { + "if": { + "exists": ".cc-cookies .cc-cookie-decline" + }, + "then": [ + { + "click": ".cc-cookies .cc-cookie-decline" + } + ], + "else": [ + { + "hide": ".cc-cookies" + } + ] + } + ] + }, { "name": "cookiefirst.com", "prehideSelectors": [ @@ -2884,16 +2953,19 @@ "detectPopup": [ { "visible": "#fides-overlay #fides-banner" + }, + { + "eval": "EVAL_FIDES_DETECT_POPUP" } ], "optIn": [ { - "waitForThenClick": "#fides-banner [data-testid=\"Accept all-btn\"]" + "waitForThenClick": "#fides-banner .fides-accept-all-button" } ], "optOut": [ { - "waitForThenClick": "#fides-banner [data-testid=\"Reject all-btn\"]" + "waitForThenClick": "#fides-banner .fides-reject-all-button" } ] }, @@ -3738,29 +3810,34 @@ }, "then": [ { - "waitForThenClick": "#lanyard_root div[class*=buttons] > button[class*=secondaryButton]", + "waitForThenClick": "#lanyard_root div[class*=buttons] > button[class*=secondaryButton], #lanyard_root button[class*=buttons-secondary]", "comment": "can be either settings or reject button" } ] }, { - "waitFor": "#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]", + "waitFor": "#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description], #ketch-preferences", "timeout": 1000, "optional": true }, { "if": { - "exists": "#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]" + "exists": "#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description], #ketch-preferences" }, "then": [ { - "waitForThenClick": "#lanyard_root button[class*=rejectButton]" + "waitForThenClick": "#lanyard_root button[class*=rejectButton], #lanyard_root button[class*=rejectAllButton]" }, { - "click": "#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)" + "click": "#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1), #lanyard_root button[class*=actionButton]" } ] } + ], + "test": [ + { + "eval": "EVAL_KETCH_TEST" + } ] }, { @@ -5162,6 +5239,42 @@ } ] }, + { + "name": "roblox", + "vendorUrl": "https://roblox.com", + "cosmetic": false, + "runContext": { + "main": true, + "frame": false, + "urlPattern": "^https://(www\\.)?roblox\\.com/" + }, + "prehideSelectors": [], + "detectCmp": [ + { + "exists": ".cookie-banner-wrapper" + } + ], + "detectPopup": [ + { + "visible": ".cookie-banner-wrapper .cookie-banner" + } + ], + "optIn": [ + { + "waitForThenClick": ".cookie-banner-wrapper button.btn-cta-lg" + } + ], + "optOut": [ + { + "waitForThenClick": ".cookie-banner-wrapper button.btn-secondary-lg" + } + ], + "test": [ + { + "eval": "EVAL_ROBLOX_TEST" + } + ] + }, { "name": "rog-forum.asus.com", "runContext": { @@ -6583,27 +6696,32 @@ }, { "name": "uswitch.com", + "runContext": { + "main": true, + "frame": false, + "urlPattern": "^https://(www\\.)?uswitch\\.com/" + }, "prehideSelectors": [ - "#cookie-banner-wrapper" + ".ucb" ], "detectCmp": [ { - "exists": "#cookie-banner-wrapper" + "exists": ".ucb-banner" } ], "detectPopup": [ { - "visible": "#cookie-banner-wrapper" + "visible": ".ucb-banner" } ], "optIn": [ { - "click": "#cookie_banner_accept_mobile" + "waitForThenClick": ".ucb-banner .ucb-btn-accept" } ], "optOut": [ { - "click": "#cookie_banner_save" + "waitForThenClick": ".ucb-banner .ucb-btn-save" } ] }, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt index 582d9a1eb87d..6d04d0b24ece 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt @@ -104,6 +104,9 @@ class AutofillManagementCredentialsMode : DuckDuckGoFragment(R.layout.fragment_a @Inject lateinit var browserNav: BrowserNav + @Inject + lateinit var stringBuilder: AutofillManagementStringBuilder + // we need to revert the toolbar title when this fragment is destroyed, so will track its initial value private var initialActionBarTitle: String? = null @@ -176,21 +179,28 @@ class AutofillManagementCredentialsMode : DuckDuckGoFragment(R.layout.fragment_a private fun launchDeleteLoginConfirmationDialog() { this.context?.let { - TextAlertDialogBuilder(it) - .setTitle(R.string.autofillDeleteLoginDialogTitle) - .setMessage(R.string.credentialManagementDeletePasswordConfirmationMessage) - .setDestructiveButtons(true) - .setPositiveButton(R.string.autofillDeleteLoginDialogDelete) - .setNegativeButton(R.string.autofillDeleteLoginDialogCancel) - .addEventListener( - object : TextAlertDialogBuilder.EventListener() { - override fun onPositiveButtonClicked() { - viewModel.onDeleteCurrentCredentials() - viewModel.onExitCredentialMode() - } - }, - ) - .show() + lifecycleScope.launch(dispatchers.io()) { + val dialogTitle = stringBuilder.stringForDeletePasswordDialogConfirmationTitle(numberToDelete = 1) + val dialogMessage = stringBuilder.stringForDeletePasswordDialogConfirmationMessage(numberToDelete = 1) + + withContext(dispatchers.main()) { + TextAlertDialogBuilder(it) + .setTitle(dialogTitle) + .setMessage(dialogMessage) + .setDestructiveButtons(true) + .setPositiveButton(R.string.autofillDeleteLoginDialogDelete) + .setNegativeButton(R.string.autofillDeleteLoginDialogCancel) + .addEventListener( + object : TextAlertDialogBuilder.EventListener() { + override fun onPositiveButtonClicked() { + viewModel.onDeleteCurrentCredentials() + viewModel.onExitCredentialMode() + } + }, + ) + .show() + } + } } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt index 4d7c3a726b6e..c51a52d61905 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt @@ -69,6 +69,7 @@ import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.mobile.android.R as CommonR import javax.inject.Inject import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @InjectWith(FragmentScope::class) class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill_management_list_mode) { @@ -100,6 +101,9 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill @Inject lateinit var deviceAuthenticator: DeviceAuthenticator + @Inject + lateinit var stringBuilder: AutofillManagementStringBuilder + val viewModel by lazy { ViewModelProvider(requireActivity(), viewModelFactory)[AutofillSettingsViewModel::class.java] } @@ -350,61 +354,54 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill private fun launchDeleteLoginConfirmationDialog(loginCredentials: LoginCredentials) { this.context?.let { - TextAlertDialogBuilder(it) - .setTitle(R.string.autofillDeleteLoginDialogTitle) - .setMessage(R.string.credentialManagementDeletePasswordConfirmationMessage) - .setDestructiveButtons(true) - .setPositiveButton(R.string.autofillDeleteLoginDialogDelete) - .setNegativeButton(R.string.autofillDeleteLoginDialogCancel) - .addEventListener( - object : TextAlertDialogBuilder.EventListener() { - override fun onPositiveButtonClicked() { - viewModel.onDeleteCredentials(loginCredentials) - } - }, - ) - .show() + lifecycleScope.launch(dispatchers.io()) { + val dialogTitle = stringBuilder.stringForDeletePasswordDialogConfirmationTitle(numberToDelete = 1) + val dialogMessage = stringBuilder.stringForDeletePasswordDialogConfirmationMessage(numberToDelete = 1) + + withContext(dispatchers.main()) { + TextAlertDialogBuilder(it) + .setTitle(dialogTitle) + .setMessage(dialogMessage) + .setDestructiveButtons(true) + .setPositiveButton(R.string.autofillDeleteLoginDialogDelete) + .setNegativeButton(R.string.autofillDeleteLoginDialogCancel) + .addEventListener( + object : TextAlertDialogBuilder.EventListener() { + override fun onPositiveButtonClicked() { + viewModel.onDeleteCredentials(loginCredentials) + } + }, + ) + .show() + } + } } } private fun launchDeleteAllLoginsConfirmationDialog(numberToDelete: Int) { - val displayStrings = getDisplayStringsForDeletingAllLogins(numberToDelete) - this.context?.let { - TextAlertDialogBuilder(it) - .setTitle(displayStrings.first) - .setMessage(displayStrings.second) - .setDestructiveButtons(true) - .setPositiveButton(R.string.autofillDeleteLoginDialogDelete) - .setNegativeButton(R.string.autofillDeleteLoginDialogCancel) - .setCancellable(true) - .addEventListener( - object : TextAlertDialogBuilder.EventListener() { - override fun onPositiveButtonClicked() { - viewModel.onDeleteAllPasswordsConfirmed() - } - }, - ) - .show() - } - } - - /** - * Returns a pair of strings for the title and message of the delete all logins confirmation dialog. - * - * The strings will change depending on if there is only one login to delete or multiple. - */ - private fun getDisplayStringsForDeletingAllLogins(numberToDelete: Int): Pair { - return if (numberToDelete == 1) { - Pair( - getString(R.string.autofillDeleteLoginDialogTitle), - getString(R.string.credentialManagementDeletePasswordConfirmationMessage), - ) - } else { - Pair( - resources.getQuantityString(R.plurals.credentialManagementDeleteAllPasswordsConfirmationTitle, numberToDelete, numberToDelete), - getString(R.string.credentialManagementDeleteAllPasswordsConfirmationMessage), - ) + lifecycleScope.launch(dispatchers.io()) { + val dialogTitle = stringBuilder.stringForDeletePasswordDialogConfirmationTitle(numberToDelete) + val dialogMessage = stringBuilder.stringForDeletePasswordDialogConfirmationMessage(numberToDelete) + + withContext(dispatchers.main()) { + TextAlertDialogBuilder(it) + .setTitle(dialogTitle) + .setMessage(dialogMessage) + .setDestructiveButtons(true) + .setPositiveButton(R.string.autofillDeleteLoginDialogDelete) + .setNegativeButton(R.string.autofillDeleteLoginDialogCancel) + .setCancellable(true) + .addEventListener( + object : TextAlertDialogBuilder.EventListener() { + override fun onPositiveButtonClicked() { + viewModel.onDeleteAllPasswordsConfirmed() + } + }, + ) + .show() + } + } } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementStringBuilder.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementStringBuilder.kt new file mode 100644 index 000000000000..2650349500c6 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementStringBuilder.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.autofill.impl.ui.credential.management.viewing + +import android.content.Context +import android.content.res.Resources +import com.duckduckgo.autofill.impl.R +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.sync.api.DeviceSyncState +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.withContext + +interface AutofillManagementStringBuilder { + fun stringForDeletePasswordDialogConfirmationTitle(numberToDelete: Int): String + suspend fun stringForDeletePasswordDialogConfirmationMessage(numberToDelete: Int): String +} + +@ContributesBinding(FragmentScope::class) +class AutofillManagementStringBuilderImpl @Inject constructor( + private val context: Context, + private val deviceSyncState: DeviceSyncState, + private val dispatchers: DispatcherProvider, +) : AutofillManagementStringBuilder { + + override fun stringForDeletePasswordDialogConfirmationTitle(numberToDelete: Int): String { + if (numberToDelete == 1) { + return context.getString(R.string.credentialManagementDeleteAllPasswordsDialogConfirmationTitleSingular) + } + + return context.resources.getQuantityString( + R.plurals.credentialManagementDeleteAllPasswordsDialogConfirmationTitlePlural, + numberToDelete, + numberToDelete, + ) + } + + override suspend fun stringForDeletePasswordDialogConfirmationMessage(numberToDelete: Int): String { + val firstMessage = context.resources.deleteAllPasswordsWarning(numberToDelete) + val secondMessage = if (numberToDelete == 1) { + context.resources.getString(R.string.credentialManagementDeleteAllSecondInstructionSingular) + } else { + context.resources.getQuantityString(R.plurals.credentialManagementDeleteAllSecondInstructionPlural, numberToDelete) + } + return "$firstMessage $secondMessage" + } + + private suspend fun Resources.deleteAllPasswordsWarning(numberToDelete: Int): String { + return withContext(dispatchers.io()) { + return@withContext if (deviceSyncState.isUserSignedInOnDevice()) { + if (numberToDelete == 1) { + getString(R.string.credentialManagementDeleteAllPasswordsFirstInstructionSyncedSingular) + } else { + getQuantityString(R.plurals.credentialManagementDeleteAllPasswordsFirstInstructionSyncedPlural, numberToDelete) + } + } else { + if (numberToDelete == 1) { + getString(R.string.credentialManagementDeleteAllPasswordsDialogFirstInstructionNotSyncedSingular) + } else { + getQuantityString(R.plurals.credentialManagementDeleteAllPasswordsDialogFirstInstructionNotSyncedPlural, numberToDelete) + } + } + } + } +} diff --git a/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml index 30cc04e0c5b7..eeaddaf8e626 100644 --- a/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Паролите се съхраняват сигурно на Вашето устройство. Добавяне на парола Заглавие - Сигурни ли сте, че искате да изтриете тази парола? Паролата е изтрита Последна актуализация %1$s Искате ли да продължите да запазвате пароли? @@ -152,13 +151,6 @@ Отмени Изтриване на всички пароли - - Сигурни ли сте, че искате да изтриете %1$d парола? - Сигурни ли сте, че искате да изтриете %1$d пароли? - - - Вашата парола ще бъде изтрита от всички синхронизирани устройства. Уверете се, че все още имате друг начин за достъп до акаунта си. - Вашите пароли ще бъдат изтрити от всички синхронизирани устройства. Уверете се, че все още имате друг начин за достъп до Вашите акаунти. %1$d изтрита парола diff --git a/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml index bda9d0ad955b..687084db4ce6 100644 --- a/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Hesla se bezpečně ukládají do tvého zařízení. Přidat heslo Název - Opravdu chceš smazat tohle heslo? Heslo smazáno Poslední aktualizace %1$s Chceš dál ukládat hesla? @@ -152,15 +151,6 @@ Zrušit Smazat všechna hesla - - Opravdu chceš smazat %1$d heslo? - Opravdu chceš smazat %1$d hesla? - Opravdu chceš smazat %1$d hesla? - Opravdu chceš smazat %1$d hesel? - - - Tvoje heslo se smaže ze všech synchronizovaných zařízení. Zkontroluj si předtím, že se i tak dostaneš ke svému účtu. - Tvoje hesla se smažou ze všech synchronizovaných zařízení. Zkontroluj si předtím, že se k účtům i tak dostaneš. %1$d smazané heslo diff --git a/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml index 4a0e04c02501..3ec7929ed60d 100644 --- a/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Adgangskoder gemmes sikkert på din enhed. Tilføj adgangskode Titel - Er du sikker på, at du vil slette denne adgangskode? Adgangskode slettet Sidst opdateret %1$s Vil du fortsætte med at gemme adgangskoder? @@ -152,13 +151,6 @@ Annuller Slet alle adgangskoder - - Er du sikker på, at du vil slette %1$d adgangskode? - Er du sikker på, at du vil slette %1$d adgangskoder? - - - Din adgangskode slettes fra alle synkroniserede enheder. Husk at sikre, at du stadig har adgang til din konto. - Dine adgangskoder slettes fra alle synkroniserede enheder. Husk at sikre, at du stadig har adgang til dine konti. %1$d adgangskode slettet diff --git a/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml index 8a735af08b69..6b95ce37042f 100644 --- a/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Passwörter werden sicher auf deinem Gerät gespeichert. Passwort hinzufügen Titel - Möchtest du dieses Passwort wirklich löschen? Passwort gelöscht Zuletzt aktualisiert %1$s Möchtest du weiterhin Passwörter speichern? @@ -152,13 +151,6 @@ Abbrechen Alle Passwörter löschen - - Möchtest du dieses %1$d Passwort wirklich löschen? - Möchtest du diese %1$d Passwörter wirklich löschen? - - - Dein Passwort wird von allen synchronisierten Geräten gelöscht. Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf dein Konto zuzugreifen. - Deine Passwörter werden von allen synchronisierten Geräten gelöscht. Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf deine Konten zuzugreifen. %1$d Passwort gelöscht diff --git a/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml index c70f97f1c3ec..0f6d9ec16961 100644 --- a/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Οι κωδικοί πρόσβασης αποθηκεύονται με ασφάλεια στη συσκευή σας. Προσθήκη κωδικού πρόσβασης Τίτλος - Θέλετε σίγουρα να διαγράψετε αυτόν τον κωδικό πρόσβασης; Διαγραφή κωδικού πρόσβασης Τελευταία ενημέρωση %1$s Θέλετε να συνεχίσετε να αποθηκεύετε κωδικούς πρόσβασης; @@ -152,13 +151,6 @@ Ακύρωση Διαγραφή όλων των κωδικών πρόσβασης - - Θέλετε σίγουρα να διαγράψετε %1$d κωδικούς πρόσβασης; - Θέλετε σίγουρα να διαγράψετε %1$d κωδικούς πρόσβασης; - - - Ο κωδικός πρόσβασής σας θα διαγραφεί από όλες τις συγχρονισμένες συσκευές. Βεβαιωθείτε ότι έχετε ακόμα τρόπο πρόσβασης στον λογαριασμό σας. - Οι κωδικοί πρόσβασής σας θα διαγραφούν από όλες τις συγχρονισμένες συσκευές. Βεβαιωθείτε ότι έχετε ακόμα τρόπο πρόσβασης στους λογαριασμούς σας. %1$d κωδικός πρόσβασης διαγράφηκε diff --git a/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml index 41226cbc371a..52a25487dd16 100644 --- a/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Las contraseñas se almacenan de forma segura en tu dispositivo. Añadir contraseña Título - ¿Seguro de que quieres borrar esta contraseña? Contraseña borrada Última actualización %1$s ¿Quieres seguir guardando contraseñas? @@ -152,13 +151,6 @@ Cancelar Borrar todas las contraseñas - - ¿Seguro de que quieres borrar %1$d contraseña? - ¿Seguro de que quieres borrar %1$d contraseñas? - - - Tu contraseña se borrará en todos los dispositivos sincronizados. Asegúrate de seguir teniendo una forma de acceder a tu cuenta. - Tus contraseñas se borrarán en todos los dispositivos sincronizados. Asegúrate de seguir teniendo una forma de acceder a tus cuentas. %1$d contraseña borrada diff --git a/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml index 8b30cdf8e4fd..5549c4f11b86 100644 --- a/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Paroolid salvestatakse turvaliselt sinu seadmesse. Parooli lisamine Pealkiri - Kas oled kindel, et soovid selle parooli kustutada? Parool on kustutatud Viimati uuendatud %1$s Kas soovid jätkata paroolide salvestamist? @@ -152,13 +151,6 @@ Loobu Kustuta kõik paroolid - - Kas soovid kindlasti %1$d parooli kustutada? - Kas soovid kindlasti %1$d parooli kustutada? - - - Sinu parool kustutatakse kõigist sünkroonitud seadmetest. Veendu, et sulle jääks endiselt mõni viis oma kontole pääsemiseks. - Sinu paroolid kustutatakse kõikidest sünkroonitud seadmetest. Veendu, et sulle jääks endiselt mõni viis oma kontodele pääsemiseks. %1$d parool kustutatud diff --git a/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml index b34a59fb8377..15ad227060ae 100644 --- a/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Salasanat tallennetaan laitteellesi turvallisesti. Lisää salasana Otsikko - Haluatko varmasti poistaa tämän salasanan? Salasana poistettu Viimeksi päivitetty %1$s Haluatko jatkaa salasanojen tallentamista? @@ -152,13 +151,6 @@ Peruuta Poista kaikki salasanat - - Haluatko varmasti poistaa %1$d salasanan? - Haluatko varmasti poistaa %1$d salasanaa? - - - Salasanasi poistetaan kaikista synkronoiduista laitteista. Varmista, että pääset yhä käyttämään tiliäsi. - Salasanasi poistetaan kaikista synkronoiduista laitteista. Varmista, että pääset yhä käyttämään tilejäsi. %1$d salasana poistettu diff --git a/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml index 6e7552a2e5d5..4c12afa4837f 100644 --- a/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Les mots de passe sont stockés en toute sécurité sur votre appareil. Ajouter un mot de passe Titre - Voulez-vous vraiment supprimer ce mot de passe ? Le mot de passe a été supprimé Dernière modification : %1$s Voulez-vous continuer à enregistrer vos mots de passe ? @@ -152,13 +151,6 @@ Annuler Supprimer tous les mots de passe - - Voulez-vous vraiment supprimer %1$d mot de passe ? - Voulez-vous vraiment supprimer %1$d mots de passe ? - - - Votre mot de passe sera supprimé de tous les appareils synchronisés. Assurez-vous d\'avoir toujours un moyen d\'accéder à votre compte. - Vos mots de passe seront supprimés de tous les appareils synchronisés. Assurez-vous d\'avoir toujours un moyen d\'accéder à vos comptes. %1$d mot de passe supprimé diff --git a/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml index c445d6ec5e89..2a8ccef04a74 100644 --- a/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Lozinke su sigurno pohranjene na tvom uređaju. Dodaj lozinku Naslov - Sigurno želiš izbrisati ovu lozinku? Lozinka je izbrisana Posljednje ažuriranje: %1$s Želiš li nastaviti spremati lozinke? @@ -152,15 +151,6 @@ Odustani Izbriši sve lozinke - - Sigurno želiš izbrisati %1$d lozinku? - Sigurno želiš izbrisati %1$d lozinke? - Sigurno želiš izbrisati %1$d lozinki? - Sigurno želiš izbrisati %1$d lozinki? - - - Tvoja lozinka bit će izbrisana sa svih sinkroniziranih uređaja. Uvjeri se da i dalje imaš način pristupa svom računu. - Tvoje lozinke bit će izbrisane sa svih sinkroniziranih uređaja. Uvjeri se da i dalje imaš način pristupa svojim računima. Izbrisana je %1$d lozinka diff --git a/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml index 7252cc0cc410..405b69f9bd74 100644 --- a/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml @@ -126,7 +126,6 @@ A jelszavakat az eszközöd biztonságosan tárolja. Jelszó hozzáadása Cím - Biztosan törlöd ezt a jelszót? Jelszó törölve Utolsó frissítés: %1$s Továbbra is szeretnéd menteni a jelszavakat? @@ -152,13 +151,6 @@ Mégsem Minden jelszó törlése - - Biztosan törölni szeretnél %1$d jelszót? - Biztosan törölni szeretnél %1$d jelszót? - - - A jelszó az összes szinkronizált eszközről törölve lesz. Győződj meg róla, hogy továbbra is hozzáférsz a fiókodhoz. - A jelszavak az összes szinkronizált eszközről törölve lesznek. Győződj meg róla, hogy továbbra is hozzáférsz a fiókjaidhoz. %1$d jelszó törölve diff --git a/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml index 1e0968dd9c58..596d42f67098 100644 --- a/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Le password sono archiviate in modo sicuro sul tuo dispositivo. Aggiungi password Titolo - Eliminare questa password? Password eliminata Ultimo aggiornamento %1$s Continuare a salvare le password? @@ -152,13 +151,6 @@ Annulla Elimina tutte le password - - Eliminare %1$d password? - Eliminare %1$d password? - - - La password verrà eliminata da tutti i dispositivi sincronizzati. Assicurati di avere ancora la possibilità di accedere al tuo account. - Le password verranno eliminate da tutti i dispositivi sincronizzati. Assicurati di avere ancora la possibilità di accedere ai tuoi account. %1$d password eliminata diff --git a/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml index 948544841c34..8dc5d3a0087f 100644 --- a/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Slaptažodžiai saugiai saugomi jūsų įrenginyje. Pridėti slaptažodį Pavadinimas - Ar tikrai norite ištrinti šį slaptažodį? Slaptažodis ištrintas Paskutinį kartą atnaujinta %1$s Ar norite toliau saugoti slaptažodžius? @@ -152,15 +151,6 @@ Atšaukti Ištrinti visus slaptažodžius - - Ar tikrai norite ištrinti %1$d slaptažodį? - Ar tikrai norite ištrinti %1$d slaptažodžius? - Ar tikrai norite ištrinti %1$d slaptažodžio? - Ar tikrai norite ištrinti %1$d slaptažodžių? - - - Jūsų slaptažodis bus ištrintas iš visų sinchronizuojamų įrenginių. Įsitikinkite, kad vis dar turite būdą, kaip pasiekti paskyrą. - Jūsų slaptažodžiai bus ištrinti iš visų sinchronizuojamų įrenginių. Įsitikinkite, kad vis dar turite būdą, kaip pasiekti paskyras. %1$d slaptažodis ištrintas diff --git a/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml index 9e2f198ef53b..5c9c426c8cc4 100644 --- a/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Paroles tiek droši glabātas tavā ierīcē. Pievienot paroli Nosaukums - Vai tiešām vēlies dzēst šo paroli? Parole dzēsta Pēdējoreiz atjaunināts %1$s Vai vēlaties turpināt saglabāt paroles? @@ -152,14 +151,6 @@ Atcelt Dzēst visas paroles - - Vai tiešām vēlies dzēst %1$d paroles? - Vai tiešām vēlies dzēst %1$d paroli? - Vai tiešām vēlies dzēst %1$d paroles? - - - Parole tiks dzēsta no visām sinhronizētajām ierīcēm. Pārliecinies, vai joprojām varēsi piekļūt savam kontam. - Paroles tiks dzēstas no visām sinhronizētajām ierīcēm. Pārliecinies, vai joprojām varēsi piekļūt savam kontam. %1$d paroles izdzēstas diff --git a/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml index 6ca5722c152d..a0f4aa4c1c35 100644 --- a/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Passord lagres på enheten din på en sikker måte. Legg til passord Tittel - Er du sikker på at du vil slette dette passordet? Passordet er slettet Sist oppdatert %1$s Vil du fortsette å lagre passord? @@ -152,13 +151,6 @@ Avbryt Slett alle passord - - Er du sikker på at du vil slette %1$d passord? - Er du sikker på at du vil slette %1$d passord? - - - Passordet ditt blir slettet fra alle synkroniserte enheter. Sørg for at du fortsatt har tilgang til kontoen din. - Passordene dine slettes fra alle synkroniserte enheter. Sørg for at du fortsatt har tilgang til kontoene dine. %1$d passord er slettet diff --git a/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml index 8f3c954de6a0..c4e5c10c2638 100644 --- a/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Wachtwoorden worden veilig opgeslagen op je apparaat. Wachtwoord toevoegen Titel - Weet je zeker dat je dit wachtwoord wilt verwijderen? Wachtwoord verwijderd Laatst bijgewerkt op %1$s Wil je wachtwoorden blijven opslaan? @@ -152,13 +151,6 @@ Annuleren Alle wachtwoorden verwijderen - - Weet je zeker dat je %1$d wachtwoord wilt verwijderen? - Weet je zeker dat je %1$d wachtwoorden wilt verwijderen? - - - Je wachtwoord wordt verwijderd van alle gesynchroniseerde apparaten. Zorg ervoor dat je nog steeds toegang hebt tot je account. - Je wachtwoorden worden verwijderd van alle gesynchroniseerde apparaten. Zorg ervoor dat je nog steeds toegang hebt tot je accounts. %1$d wachtwoord verwijderd diff --git a/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml index f08ebfb86ef2..a9fd6831cf37 100644 --- a/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Hasła są bezpiecznie przechowywane na Twoim urządzeniu. Dodaj hasło Tytuł - Czy na pewno chcesz usunąć to hasło? Hasło usunięte Ostatnia aktualizacja %1$s Czy chcesz nadal zapisywać hasła? @@ -152,15 +151,6 @@ Anuluj Usuń wszystkie hasła - - Czy na pewno chcesz usunąć %1$d hasło? - Czy na pewno chcesz usunąć %1$d hasła? - Czy na pewno chcesz usunąć %1$d haseł? - Czy na pewno chcesz usunąć %1$d hasła? - - - Twoje hasło zostanie usunięte ze wszystkich zsynchronizowanych urządzeń. Upewnij się, że nadal masz możliwość dostępu do swojego konta. - Twoje hasła zostaną usunięte ze wszystkich zsynchronizowanych urządzeń. Upewnij się, że nadal masz możliwość dostępu do swoich kont. Usunięto %1$d hasło diff --git a/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml index 4ce154035d7a..90abbc4173c0 100644 --- a/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml @@ -126,7 +126,6 @@ As palavras-passe são armazenadas com segurança no teu dispositivo. Adicionar palavra-passe Título - Tens a certeza de que pretendes eliminar esta palavra-passe? Palavra-passe eliminada Última atualização a %1$s Pretendes continuar a guardar palavras-passe? @@ -152,13 +151,6 @@ Cancelar Eliminar todas as palavras-passe - - Tens a certeza de que pretendes eliminar %1$d palavra-passe? - Tens a certeza de que pretendes eliminar %1$d palavras-passe? - - - A tua palavra-passe será eliminada de todos os dispositivos sincronizados. Confirma que ainda tens uma forma de aceder à conta. - As tuas palavras-passe serão eliminadas de todos os dispositivos sincronizados. Confirma que ainda tens uma forma de aceder às contas. %1$d palavra-passe eliminada diff --git a/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml index dc635070746f..fb357086cc37 100644 --- a/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Parolele sunt stocate în siguranță pe dispozitivul tău. Adaugă parola Titlu - Sigur dorești să ștergi această parolă? Parolă ștearsă Ultima actualizare %1$s Dorești să continui să salvezi parolele? @@ -152,14 +151,6 @@ Anulare Șterge toate parolele - - Sigur dorești să ștergi %1$d parolă? - Sigur dorești să ștergi %1$d parole? - Sigur dorești să ștergi această %1$d de parole? - - - Parola ta va fi ștearsă de pe toate dispozitivele sincronizate. Asigură-te că ai în continuare posibilitatea de a-ți accesa contul. - Parolele tale vor fi șterse de pe toate dispozitivele sincronizate. Asigură-te că ai în continuare posibilitatea de a-ți accesa conturile. %1$d parolă ștearsă diff --git a/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml index 6ac5672c8147..7df59927d55f 100644 --- a/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Пароли надежно защищены и хранятся на вашем устройстве. Добавление пароля Название - Вы точно хотите удалить этот пароль? Пароль удален Последнее обновление: %1$s Продолжить сохранять пароли? @@ -152,15 +151,6 @@ Отменить Удалить все пароли - - Вы точно хотите удалить %1$d пароль? - Вы точно хотите удалить %1$d пароля? - Вы точно хотите удалить %1$d паролей? - Вы точно хотите удалить пароли (%1$d)? - - - Пароль будет удален со всех синхронизированных устройств. Обязательно убедитесь, что вы можете войти в свою учетную запись другим способом. - Пароли будут удалены со всех синхронизированных устройств. Убедитесь, что вы по-прежнему можете войти в свои учетные записи. Удален %1$d пароль diff --git a/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml index 0ffbe0cb2600..49cbd0029a60 100644 --- a/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Heslá sú zabezpečeným spôsobom uložené vo vašom zariadení. Pridať heslo Názov - Naozaj chcete odstrániť toto heslo? Heslo bolo odstránené Naposledy aktualizované %1$s Chcete pokračovať v ukladaní hesiel? @@ -152,15 +151,6 @@ Zrušiť Odstránenie všetkých hesiel - - Naozaj chcete odstrániť %1$d heslo? - Naozaj chcete odstrániť %1$d heslá? - Naozaj chcete odstrániť %1$d hesla? - Naozaj chcete odstrániť %1$d hesiel? - - - Vaše heslo sa odstráni zo všetkých synchronizovaných zariadení. Zabezpečte si prístup k svojmu účtu. - Vaše heslá sa odstránia zo všetkých synchronizovaných zariadení. Zabezpečte si prístup k svojim účtom. Bolo odstránené %1$d heslo diff --git a/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml index 5d17b40a3f14..942a427489f9 100644 --- a/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Gesla so varno shranjena v vaši napravi. Dodajte geslo Naslov - Ali ste prepričani, da želite izbrisati to geslo? Geslo je izbrisano Nazadnje posodobljeno dne %1$s Ali želite še naprej shranjevati gesla? @@ -152,15 +151,6 @@ Prekliči Brisanje vseh gesel - - Ali ste prepričani, da želite izbrisati %1$d geslo? - Ali ste prepričani, da želite izbrisati %1$d gesli? - Ali ste prepričani, da želite izbrisati %1$d gesla? - Ali ste prepričani, da želite izbrisati %1$d gesel? - - - Vaše geslo bo izbrisano iz vseh sinhroniziranih naprav. Prepričajte se, da imate še vedno možnost dostopa do svojega računa. - Vaša gesla bodo izbrisana iz vseh sinhroniziranih naprav. Prepričajte se, da imate še vedno možnost dostopa do svojih računov. %1$d geslo je bilo izbrisano diff --git a/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml index ce3fe1e1d21b..3582cc363d18 100644 --- a/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Lösenord lagras säkert på din enhet. Lägg till lösenord Rubrik - Är du säker på att du vill radera detta lösenord? Lösenordet har raderats Uppdaterades senast %1$s Vill du fortsätta spara lösenord? @@ -152,13 +151,6 @@ Avbryt Radera alla lösenord - - Är du säker på att du vill radera %1$d lösenord? - Är du säker på att du vill radera %1$d lösenord? - - - Ditt lösenord kommer att raderas från alla synkroniserade enheter. Se till att du har kvar ett sätt att komma åt ditt konto. - Dina lösenord raderas från alla synkroniserade enheter. Se till att du har kvar ett sätt att komma åt dina konton. %1$d lösenord raderat diff --git a/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml index 050e8708f509..1dcb771c96dc 100644 --- a/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Şifreler cihazınızda güvenli bir şekilde saklanır. Şifre Ekle Title - Bu şifreyi silmek istediğinizden emin misiniz? Şifre silindi Son güncelleme %1$s Şifreleri kaydetmeye devam etmek istiyor musunuz? @@ -152,13 +151,6 @@ Vazgeç Tüm Şifreleri Sil - - Bu %1$d şifreyi silmek istediğinizden emin misiniz? - Bu %1$d şifreyi silmek istediğinizden emin misiniz? - - - Şifreniz senkronize edilen tüm cihazlardan silinecek. Hesabınıza başka bir şekilde erişebilecek olduğunuzdan emin olun. - Şifreleriniz senkronize edilen tüm cihazlardan silinecektir. Hesaplarınıza başka bir şekilde erişebilecek olduğunuzdan emin olun. %1$d şifre silindi diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml index c8b768fb87e2..146be810ce81 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -15,4 +15,24 @@ --> + + Your password will be deleted from this device. + + Your passwords will be deleted from this device. + + + Your password will be deleted from all synced devices. + + Your passwords will be deleted from all synced devices. + + + Are you sure you want to delete this password? + + Are you sure you want to delete %1$d passwords? + + + Make sure you still have a way to access your account. + + Make sure you still have a way to access your accounts. + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml index 8d3bfcd22371..08dee3cd0557 100644 --- a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml @@ -126,7 +126,6 @@ Passwords are stored securely on your device. Add Password Title - Are you sure you want to delete this password? Password deleted Last updated %1$s Do you want to keep saving passwords? @@ -152,12 +151,6 @@ Cancel Delete All Passwords - - Are you sure you want to delete %1$d passwords? - - - Your password will be deleted from all synced devices. Make sure you still have a way to access your account. - Your passwords will be deleted from all synced devices. Make sure you still have a way to access your accounts. %1$d password deleted diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementStringBuilderImplTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementStringBuilderImplTest.kt new file mode 100644 index 000000000000..29b9005c639c --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementStringBuilderImplTest.kt @@ -0,0 +1,87 @@ +package com.duckduckgo.autofill.impl.ui.credential.management.viewing + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.sync.api.DeviceSyncState +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class AutofillManagementStringBuilderImplTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val deviceSyncState: DeviceSyncState = mock() + + private val testee = AutofillManagementStringBuilderImpl( + context = context, + deviceSyncState = deviceSyncState, + dispatchers = coroutineTestRule.testDispatcherProvider, + ) + + @Test + fun whenDeletingOneLoginThenBuildsCorrectTitle() { + val str = testee.stringForDeletePasswordDialogConfirmationTitle(numberToDelete = 1) + assertEquals(DELETE_DIALOG_TITLE_1_PASSWORD, str) + } + + @Test + fun whenDeletingMultipleLoginsThenBuildsCorrectTitle() { + val str = testee.stringForDeletePasswordDialogConfirmationTitle(numberToDelete = 2) + assertEquals(DELETE_DIALOG_TITLE_2_PASSWORDS, str) + } + + @Test + fun whenDeletingOneLoginWithSyncEnabledThenBuildsCorrectMessage() = runTest { + configureSyncState(enabled = true) + val str = testee.stringForDeletePasswordDialogConfirmationMessage(numberToDelete = 1) + assertEquals("$DELETE_DIALOG_MESSAGE_1_SYNC_ENABLED_SINGULAR $DELETE_DIALOG_MESSAGE_2_SINGULAR", str) + } + + @Test + fun whenDeletingOneLoginWithSyncDisabledThenBuildsCorrectMessage() = runTest { + configureSyncState(enabled = false) + val str = testee.stringForDeletePasswordDialogConfirmationMessage(numberToDelete = 1) + assertEquals("$DELETE_DIALOG_MESSAGE_1_SYNC_DISABLED_SINGULAR $DELETE_DIALOG_MESSAGE_2_SINGULAR", str) + } + + @Test + fun whenDeletingTwoLoginsWithSyncEnabledThenBuildsCorrectMessage() = runTest { + configureSyncState(enabled = true) + val str = testee.stringForDeletePasswordDialogConfirmationMessage(numberToDelete = 2) + assertEquals("$DELETE_DIALOG_MESSAGE_1_SYNC_ENABLED_PLURAL $DELETE_DIALOG_MESSAGE_2_PLURAL", str) + } + + @Test + fun whenDeletingTwoLoginsWithSyncDisabledThenBuildsCorrectMessage() = runTest { + configureSyncState(enabled = false) + val str = testee.stringForDeletePasswordDialogConfirmationMessage(numberToDelete = 2) + assertEquals("$DELETE_DIALOG_MESSAGE_1_SYNC_DISABLED_PLURAL $DELETE_DIALOG_MESSAGE_2_PLURAL", str) + } + + private fun configureSyncState(enabled: Boolean) { + whenever(deviceSyncState.isUserSignedInOnDevice()).thenReturn(enabled) + } + + private companion object { + private const val DELETE_DIALOG_TITLE_1_PASSWORD = "Are you sure you want to delete this password?" + private const val DELETE_DIALOG_TITLE_2_PASSWORDS = "Are you sure you want to delete 2 passwords?" + + private const val DELETE_DIALOG_MESSAGE_1_SYNC_ENABLED_SINGULAR = "Your password will be deleted from all synced devices." + private const val DELETE_DIALOG_MESSAGE_1_SYNC_ENABLED_PLURAL = "Your passwords will be deleted from all synced devices." + + private const val DELETE_DIALOG_MESSAGE_1_SYNC_DISABLED_SINGULAR = "Your password will be deleted from this device." + private const val DELETE_DIALOG_MESSAGE_1_SYNC_DISABLED_PLURAL = "Your passwords will be deleted from this device." + + private const val DELETE_DIALOG_MESSAGE_2_SINGULAR = "Make sure you still have a way to access your account." + private const val DELETE_DIALOG_MESSAGE_2_PLURAL = "Make sure you still have a way to access your accounts." + } +} diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/DaxBubbleCardView.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/DaxBubbleCardView.kt index 1a69d7b6d770..10d4cd465c9d 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/DaxBubbleCardView.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/DaxBubbleCardView.kt @@ -33,15 +33,42 @@ constructor( ) : MaterialCardView(context, attrs, defStyleAttr) { init { + val attr = context.theme.obtainStyledAttributes(attrs, R.styleable.DaxBubbleCardView, defStyleAttr, 0) + val edgePosition = EdgePosition.from(attr.getInt(R.styleable.DaxBubbleCardView_edgePosition, 0)) + val cornderRadius = resources.getDimension(R.dimen.mediumShapeCornerRadius) val cornerSize = resources.getDimension(R.dimen.daxBubbleDialogEdge) val distanceFromEdge = resources.getDimension(R.dimen.daxBubbleDialogDistanceFromEdge) - val edgeTreatment = DaxBubbleEdgeTreatment(cornerSize, distanceFromEdge) + val edgeTreatment = DaxBubbleEdgeTreatment(cornerSize, distanceFromEdge, edgePosition) setCardBackgroundColor(ColorStateList.valueOf(context.getColorFromAttr(R.attr.daxColorSurface))) - shapeAppearanceModel = ShapeAppearanceModel.builder() - .setAllCornerSizes(cornderRadius) - .setTopEdge(edgeTreatment) - .build() + + shapeAppearanceModel = when (edgePosition) { + EdgePosition.TOP -> ShapeAppearanceModel.builder() + .setAllCornerSizes(cornderRadius) + .setTopEdge(edgeTreatment) + .build() + + EdgePosition.LEFT -> ShapeAppearanceModel.builder() + .setAllCornerSizes(cornderRadius) + .setLeftEdge(edgeTreatment) + .build() + } + } + + enum class EdgePosition { + TOP, + LEFT, + ; + + companion object { + fun from(value: Int): EdgePosition { + // same order as attrs-dax-dialog.xml + return when (value) { + 1 -> LEFT + else -> TOP + } + } + } } } diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/Shapes.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/Shapes.kt index 75c33fea4253..370961c9e7b5 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/Shapes.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/shape/Shapes.kt @@ -16,6 +16,7 @@ package com.duckduckgo.common.ui.view.shape +import com.duckduckgo.common.ui.view.shape.DaxBubbleCardView.EdgePosition import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.EdgeTreatment import com.google.android.material.shape.MaterialShapeDrawable @@ -31,9 +32,12 @@ class DaxBubbleEdgeTreatment * of the side of the triangle coincident with the rest of the edge is 2 * size. * @param inside true if the triangle should be "cut out" of the shape (i.e. inward-facing); false * if the triangle should extend out of the shape. + * @param edgePosition TOP for positioning triangle on the top side of the dialog; LEFT for + * positioning triangle on the left side of the dialog */( private val size: Float, private val distanceFromEdge: Float, + private val edgePosition: EdgePosition = EdgePosition.TOP, ) : EdgeTreatment() { override fun getEdgePath( length: Float, @@ -41,10 +45,22 @@ class DaxBubbleEdgeTreatment interpolation: Float, shapePath: ShapePath, ) { - shapePath.lineTo(distanceFromEdge - size * interpolation, 0f) - shapePath.lineTo(distanceFromEdge, -size * interpolation) - shapePath.lineTo(distanceFromEdge + size * interpolation, 0f) - shapePath.lineTo(length, 0f) + when (edgePosition) { + EdgePosition.TOP -> { + shapePath.lineTo(distanceFromEdge - size * interpolation, 0f) + shapePath.lineTo(distanceFromEdge, -size * interpolation) + shapePath.lineTo(distanceFromEdge + size * interpolation, 0f) + shapePath.lineTo(length, 0f) + } + + EdgePosition.LEFT -> { + val d = length - distanceFromEdge + shapePath.lineTo(d - size * interpolation, 0f) + shapePath.lineTo(d, -size * interpolation) + shapePath.lineTo(d + size * interpolation, 0f) + shapePath.lineTo(length, 0f) + } + } } } diff --git a/app/src/main/res/drawable-w600dp/onboarding_background.xml b/common/common-ui/src/main/res/values/attrs-dax-dialog.xml similarity index 58% rename from app/src/main/res/drawable-w600dp/onboarding_background.xml rename to common/common-ui/src/main/res/values/attrs-dax-dialog.xml index c0e7b377b464..1b6e9b2335fa 100644 --- a/app/src/main/res/drawable-w600dp/onboarding_background.xml +++ b/common/common-ui/src/main/res/values/attrs-dax-dialog.xml @@ -1,6 +1,5 @@ - - - \ No newline at end of file + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-loader/library-loader-api/build.gradle b/library-loader/library-loader-api/build.gradle index 0a2fd6dcc9d0..81d8ca45bc43 100644 --- a/library-loader/library-loader-api/build.gradle +++ b/library-loader/library-loader-api/build.gradle @@ -23,7 +23,7 @@ apply from: "$rootProject.projectDir/gradle/android-library.gradle" dependencies { - implementation "com.getkeepsafe.relinker:relinker:_" + api "com.getkeepsafe.relinker:relinker:_" } android { diff --git a/library-loader/library-loader-api/src/main/java/com/duckduckgo/library/loader/LibraryLoader.kt b/library-loader/library-loader-api/src/main/java/com/duckduckgo/library/loader/LibraryLoader.kt index e973c8f123a4..f761a852ac5e 100644 --- a/library-loader/library-loader-api/src/main/java/com/duckduckgo/library/loader/LibraryLoader.kt +++ b/library-loader/library-loader-api/src/main/java/com/duckduckgo/library/loader/LibraryLoader.kt @@ -18,11 +18,18 @@ package com.duckduckgo.library.loader import android.content.Context import com.getkeepsafe.relinker.ReLinker +import com.getkeepsafe.relinker.ReLinker.LoadListener class LibraryLoader { companion object { fun loadLibrary(context: Context, name: String) { ReLinker.loadLibrary(context, name) } + + fun loadLibrary(context: Context, name: String, listener: LibraryLoaderListener) { + ReLinker.loadLibrary(context, name, listener) + } } + + interface LibraryLoaderListener : LoadListener } diff --git a/lint-rules/src/main/java/com/duckduckgo/lint/WrongPluginPointCollectorDetector.kt b/lint-rules/src/main/java/com/duckduckgo/lint/WrongPluginPointCollectorDetector.kt index f60e7af6f4b2..0e54c402d9c4 100644 --- a/lint-rules/src/main/java/com/duckduckgo/lint/WrongPluginPointCollectorDetector.kt +++ b/lint-rules/src/main/java/com/duckduckgo/lint/WrongPluginPointCollectorDetector.kt @@ -84,7 +84,7 @@ class WrongPluginPointCollectorDetector : Detector(), SourceCodeScanner { } private fun PsiClass.isActivePlugin(): Boolean { - return this.isSubtypeOf("com.duckduckgo.common.utils.plugins.ActivePluginPoint.ActivePlugin") + return this.isSubtypeOf("com.duckduckgo.common.utils.plugins.ActivePlugin") } private fun handleField(node: UField) { node.type.let { psiType -> @@ -95,7 +95,7 @@ class WrongPluginPointCollectorDetector : Detector(), SourceCodeScanner { for (typeArgument in typeArguments) { val typeArgumentClass = (typeArgument as? PsiClassType)?.resolve() if (typeArgumentClass?.isSubtypeOf( - "com.duckduckgo.common.utils.plugins.ActivePluginPoint.ActivePlugin" + "com.duckduckgo.common.utils.plugins.ActivePlugin" ) == true) { context.reportError(node, WRONG_PLUGIN_POINT_ISSUE) } diff --git a/lint-rules/src/test/java/com/duckduckgo/lint/WrongPluginPointCollectorDetectorTest.kt b/lint-rules/src/test/java/com/duckduckgo/lint/WrongPluginPointCollectorDetectorTest.kt index dbed099b65ea..4e4bfd97287e 100644 --- a/lint-rules/src/test/java/com/duckduckgo/lint/WrongPluginPointCollectorDetectorTest.kt +++ b/lint-rules/src/test/java/com/duckduckgo/lint/WrongPluginPointCollectorDetectorTest.kt @@ -19,40 +19,40 @@ package com.duckduckgo.lint import com.android.tools.lint.checks.infrastructure.TestFiles.kt import com.android.tools.lint.checks.infrastructure.TestLintTask.lint import com.duckduckgo.lint.WrongPluginPointCollectorDetector.Companion.WRONG_PLUGIN_POINT_ISSUE +import com.duckduckgo.lint.utils.PLUGIN_POINT_ANNOTATIONS_API +import com.duckduckgo.lint.utils.PLUGIN_POINT_API import org.junit.Test class WrongPluginPointCollectorDetectorTest { @Test fun `test normal plugin point constructor parameter collecting active plugins`() { lint() - .files(kt(""" - package com.duckduckgo.common.utils.plugins - - interface PluginPoint { - fun getPlugins(): Collection - } - - interface ActivePluginPoint { - interface ActivePlugin { - suspend fun isActive(): Boolean = true - } - } - - interface MyPlugin - interface MyPluginActivePlugin : ActivePluginPoint.ActivePlugin + .files( + PLUGIN_POINT_API, + PLUGIN_POINT_ANNOTATIONS_API, + kt(""" + package com.test.plugins - class Duck(private val pp: PluginPoint) { - fun quack() { + import com.duckduckgo.common.utils.plugins.ActivePlugin + import com.duckduckgo.common.utils.plugins.PluginPoint + import com.duckduckgo.anvil.annotations.ContributesActivePlugin + + interface MyPlugin + interface MyPluginActivePlugin : ActivePlugin + + class Duck(private val pp: PluginPoint) { + fun quack() { + } } - } - """).indented()) + """).indented() + ) .issues(WRONG_PLUGIN_POINT_ISSUE) .run() .expect(""" - src/com/duckduckgo/common/utils/plugins/PluginPoint.kt:16: Error: PluginPoint cannot be collector of ActivePlugin(s) [WrongPluginPointCollectorDetector] + src/com/test/plugins/MyPlugin.kt:10: Error: PluginPoint cannot be collector of ActivePlugin(s) [WrongPluginPointCollectorDetector] class Duck(private val pp: PluginPoint) { ~~~~ - src/com/duckduckgo/common/utils/plugins/PluginPoint.kt:16: Error: PluginPoint cannot be collector of ActivePlugin(s) [WrongPluginPointCollectorDetector] + src/com/test/plugins/MyPlugin.kt:10: Error: PluginPoint cannot be collector of ActivePlugin(s) [WrongPluginPointCollectorDetector] class Duck(private val pp: PluginPoint) { ~~ 2 errors, 0 warnings @@ -62,26 +62,23 @@ class WrongPluginPointCollectorDetectorTest { @Test fun `test active plugin point constructor parameter collecting active plugins`() { lint() - .files(kt(""" - package com.duckduckgo.common.utils.plugins - - interface PluginPoint { - fun getPlugins(): Collection - } - - interface ActivePluginPoint { - interface ActivePlugin { - suspend fun isActive(): Boolean = true - } - } - - interface MyPlugin - interface MyPluginActivePlugin : ActivePluginPoint.ActivePlugin + .files( + PLUGIN_POINT_API, + PLUGIN_POINT_ANNOTATIONS_API, + kt(""" + package com.test.plugins + + import com.duckduckgo.common.utils.plugins.ActivePlugin + import com.duckduckgo.common.utils.plugins.PluginPoint + import com.duckduckgo.anvil.annotations.ContributesActivePlugin - class Duck(private val pp: PluginPoint) { - fun quack() { + interface MyPlugin + interface MyPluginActivePlugin : ActivePlugin + + class Duck(private val pp: PluginPoint) { + fun quack() { + } } - } """).indented()) .issues(WRONG_PLUGIN_POINT_ISSUE) .run() @@ -91,33 +88,30 @@ class WrongPluginPointCollectorDetectorTest { @Test fun `test normal plugin point field collecting active plugins`() { lint() - .files(kt(""" - package com.duckduckgo.common.utils.plugins - - interface PluginPoint { - fun getPlugins(): Collection - } - - interface ActivePluginPoint { - interface ActivePlugin { - suspend fun isActive(): Boolean = true - } - } + .files( + PLUGIN_POINT_API, + PLUGIN_POINT_ANNOTATIONS_API, + kt(""" + package com.test.plugins - interface MyPlugin - interface MyPluginActivePlugin : ActivePluginPoint.ActivePlugin + import com.duckduckgo.common.utils.plugins.ActivePlugin + import com.duckduckgo.common.utils.plugins.PluginPoint + import com.duckduckgo.anvil.annotations.ContributesActivePlugin - class Duck { - private val pp: PluginPoint - - fun quack() { + interface MyPlugin + interface MyPluginActivePlugin : ActivePlugin + + class Duck { + private val pp: PluginPoint + + fun quack() { + } } - } """).indented()) .issues(WRONG_PLUGIN_POINT_ISSUE) .run() .expect(""" - src/com/duckduckgo/common/utils/plugins/PluginPoint.kt:17: Error: PluginPoint cannot be collector of ActivePlugin(s) [WrongPluginPointCollectorDetector] + src/com/test/plugins/MyPlugin.kt:11: Error: PluginPoint cannot be collector of ActivePlugin(s) [WrongPluginPointCollectorDetector] private val pp: PluginPoint ~~ 1 errors, 0 warnings @@ -127,28 +121,22 @@ class WrongPluginPointCollectorDetectorTest { @Test fun `test active plugin point field collecting active plugins`() { lint() - .files(kt(""" - package com.duckduckgo.common.utils.plugins - - interface PluginPoint { - fun getPlugins(): Collection - } - - interface ActivePluginPoint { - interface ActivePlugin { - suspend fun isActive(): Boolean = true - } - } - - interface MyPlugin - interface MyPluginActivePlugin : ActivePluginPoint.ActivePlugin + .files( + PLUGIN_POINT_API, + PLUGIN_POINT_ANNOTATIONS_API, + kt(""" + package com.test.plugins - class Duck { - private val pp: PluginPoint - - fun quack() { + import com.duckduckgo.common.utils.plugins.ActivePlugin + import com.duckduckgo.common.utils.plugins.PluginPoint + import com.duckduckgo.anvil.annotations.ContributesActivePlugin + + class Duck { + private val pp: PluginPoint + + fun quack() { + } } - } """).indented()) .issues(WRONG_PLUGIN_POINT_ISSUE) .run() diff --git a/lint-rules/src/test/java/com/duckduckgo/lint/utils/PluginUtils.kt b/lint-rules/src/test/java/com/duckduckgo/lint/utils/PluginUtils.kt new file mode 100644 index 000000000000..13ad9fa3f9de --- /dev/null +++ b/lint-rules/src/test/java/com/duckduckgo/lint/utils/PluginUtils.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.lint.utils + +import com.android.tools.lint.checks.infrastructure.TestFiles + +internal val PLUGIN_POINT_API = TestFiles.kt( + """ + package com.duckduckgo.common.utils.plugins + + interface PluginPoint { + fun getPlugins(): Collection + } + interface InternalActivePluginPoint { + suspend fun getPlugins(): Collection + } + interface ActivePlugin { + suspend fun isActive(): Boolean = true + } + typealias ActivePluginPoint = InternalActivePluginPoint<@JvmSuppressWildcards T> + """ +).indented() +internal val PLUGIN_POINT_ANNOTATIONS_API = TestFiles.kt( + """ + package com.duckduckgo.anvil.annotations + + annotation class ContributesActivePlugin + annotation class ContributesActivePluginPoint( + val boundType: KClass<*> = Unit::class, + ) + + """ +).indented() diff --git a/package-lock.json b/package-lock.json index 65a0a4fa28f7..726dacc900a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "ddg-android", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.8.0", + "@duckduckgo/autoconsent": "^10.9.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#10.2.0", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#5.17.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#5.18.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1708702034" }, @@ -23,12 +23,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.6", "picocolors": "^1.0.0" }, "engines": { @@ -36,21 +36,21 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.6", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -60,16 +60,16 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.8.0.tgz", - "integrity": "sha512-n4axPmOsDxK9X6UYUb+S7KWqvppG75IemnH+pK1SAp01+US4ez+t7fzq4VQU19dy0EpGyC1bUnsB2BzdhUx65g==" + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.9.0.tgz", + "integrity": "sha512-eTlPFmW7QzThb9OGDWSSnUmB8Kq74YDO1N63cC/XsBHruaeN13N60GwhcL2/p4C05Gfkv8AhyOl/fTvUjya/hg==" }, "node_modules/@duckduckgo/autofill": { "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#6493e296934bf09277c03df45f11f4619711cb24", "hasInstallScript": true }, "node_modules/@duckduckgo/content-scope-scripts": { - "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#fa861c4eccb21d235e34070b208b78bdc32ece08", + "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#1f563c01d5da4777120b672d7473c36947a0c727", "hasInstallScript": true, "workspaces": [ "packages/special-pages", @@ -333,12 +333,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -471,9 +471,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -529,6 +529,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -621,6 +622,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -771,12 +773,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { diff --git a/package.json b/package.json index 3fa8785a8388..76d22c97002a 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.8.0", + "@duckduckgo/autoconsent": "^10.9.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#10.2.0", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#5.17.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#5.18.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1708702034" }