From 56a5542dcf2d84a1c7eb9bab635dda1c34c3a1f4 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:53:24 -0400 Subject: [PATCH 01/48] Release 7.128.0-0 (#3058) --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 22 ++++++-- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++++++---------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- fastlane/metadata/default/release_notes.txt | 8 +-- 6 files changed, 52 insertions(+), 42 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 657865d520..cd6762ec6d 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.127.0 +MARKETING_VERSION = 7.128.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index 5f5446df62..fdeb22501d 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"8d80c53fc5696854e2ef43bcb28089a1\"" - public static let embeddedDataSHA = "cdc42bdffa7ab5478ca80febf325cf689148cd19a68153e0a2cafed888b1d1ce" + public static let embeddedDataETag = "\"889258698061231074e85e982257c377\"" + public static let embeddedDataSHA = "7e915bd1830ef7762b731e62a61c9c3f53f79e2f247d169dd70b45b1121f93d0" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index 5d30590562..d910ca5022 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1719853254811, + "version": 1720185364708, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -115,6 +115,11 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "androidNewStateKillSwitch": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "autoconsent": { "exceptions": [ { @@ -1159,6 +1164,9 @@ { "domain": "soranews24.com" }, + { + "domain": "pointstreaksites.com" + }, { "domain": "marvel.com" }, @@ -1169,7 +1177,7 @@ "domain": "noaprints.com" } ], - "hash": "ee9d383becfe28d8a5b575b51f5c9d11" + "hash": "477f58bcbc508d5ae1e73941407284b7" }, "cookie": { "settings": { @@ -1291,6 +1299,9 @@ "features": { "pip": { "state": "disabled" + }, + "autoplay": { + "state": "disabled" } }, "settings": { @@ -1357,7 +1368,7 @@ ] }, "state": "disabled", - "hash": "314df3f87ca501a86eb1d04db8bf7bbc" + "hash": "d950da7e94382e26e5da62e52ce3ac79" }, "elementHiding": { "exceptions": [ @@ -6213,7 +6224,8 @@ "domains": [ "ah.nl", "applesfera.com", - "rocketnews24.com" + "rocketnews24.com", + "servustv.com" ] }, { @@ -8665,7 +8677,7 @@ "domain": "noaprints.com" } ], - "hash": "17e4849349a0968a744a171fbcc09f1d" + "hash": "d7d18b84410856df8bf3fb5e2a1f2e56" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d99b9972f5..57fc9a1480 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8169,7 +8169,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8206,7 +8206,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8296,7 +8296,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8323,7 +8323,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8472,7 +8472,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8497,7 +8497,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8566,7 +8566,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8600,7 +8600,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8633,7 +8633,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8663,7 +8663,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8973,7 +8973,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9004,7 +9004,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9032,7 +9032,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9065,7 +9065,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9095,7 +9095,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9128,11 +9128,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9365,7 +9365,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9392,7 +9392,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9424,7 +9424,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9461,7 +9461,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9496,7 +9496,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9531,11 +9531,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9708,11 +9708,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9741,10 +9741,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 64d4cdd2c2..25d6870138 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.127.0 + 7.128.0 Key version Title diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index b4dad65e3a..5d700d36bd 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,5 +1,3 @@ -- Bug fixes and other improvements. - -For Privacy Pro subscribers: -- You can now specify a custom DNS server under VPN Settings to route your DNS queries through while the VPN is active. - +- Fixed an issue where cancelling downloads would sometimes not work as expected. +- Make sure you have iOS 15 or later to keep getting the latest browser updates and improvements. Here's how to update iOS: https://support.apple.com/guide/iphone/update-ios-iph3e504502/14.0/ios/14.0 +Join our fully distributed team and help raise the standard of trust online! https://duckduckgo.com/hiring \ No newline at end of file From 259cd88f5b0b9d29ee1d3ee93785d82f3d32ebb9 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:44:54 -0400 Subject: [PATCH 02/48] Die when git push fails (#3052) --- scripts/prepare_release.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index be43dafeef..dca7095571 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -104,8 +104,8 @@ process_release() { # expected input e.g. "1.72.0" echo "Processing version number: $version" - if release_branch_exists; then - is_subsequent_release=1 + if release_branch_exists; then + is_subsequent_release=1 base_branch="$release_branch" fi } @@ -115,10 +115,10 @@ process_hotfix() { # expected input e.g. "hotfix/1.72.1" release_branch="$1" base_branch="$1" is_hotfix=1 - + echo "Processing hotfix branch name: $release_branch" - if ! release_branch_exists; then + if ! release_branch_exists; then die "💥 Error: Hotfix branch ${release_branch} does not exist. It should be created before you run this script." fi } @@ -144,7 +144,9 @@ create_release_branch() { fi eval git checkout -b "${release_branch}" "$mute" - eval git push -u origin "${release_branch}" "$mute" + if ! eval git push -u origin "${release_branch}" "$mute"; then + die "💥 Error: Failed to push ${release_branch} to origin" + fi echo "✅" } @@ -165,7 +167,10 @@ create_build_branch() { fi eval git checkout -b "${build_branch}" "$mute" - eval git push -u origin "${build_branch}" "$mute" + if ! eval git push -u origin "${build_branch}" "$mute"; then + die "💥 Error: Failed to push ${build_branch} to origin" + fi + echo "✅" } @@ -237,7 +242,7 @@ main() { read_command_line_arguments "$@" checkout_base_branch - if [[ $is_subsequent_release ]]; then + if [[ $is_subsequent_release ]]; then create_build_branch elif [[ $is_hotfix ]]; then create_build_branch From a68d0f931e7748ccffd731ed56c75b95b9929ab8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 08:41:00 +0200 Subject: [PATCH 03/48] Remote Messaging Framework for macOS (#3031) Task/Issue URL: https://app.asana.com/0/72649045549333/1202913520695928/f Description: This change adjusts RMF implementation to updates in BSK that add support for RMF on macOS. * RMF is now controlled by a feature flag, enabled by default on iOS. * Remote messaging config matcher creation has been moved into a standalone provider class that is otherwise separated from the RMF client. * RemoteMessagingClient now implements a protocol from BSK that provides implementation for fetching and processing the config. The API is not static and the instance of the client is owned by AppDelegate. * RemoteMessagingStore and HomePageConfiguration were moved out of AppDependencyProvider to have better control over their instantiation time and to allow for dependency injection. --- Core/AppConfigurationURLProvider.swift | 1 + Core/Configuration.swift | 1 + DuckDuckGo.xcodeproj/project.pbxproj | 10 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- DuckDuckGo/AppDelegate.swift | 32 ++- DuckDuckGo/AppDependencyProvider.swift | 9 - .../ConfigurationDebugViewController.swift | 1 + .../ConfigurationURLDebugViewController.swift | 2 + DuckDuckGo/HomeCollectionView.swift | 5 +- DuckDuckGo/HomePageConfiguration.swift | 15 +- DuckDuckGo/HomeViewController.swift | 15 +- DuckDuckGo/MainViewController.swift | 12 +- DuckDuckGo/NewTabPageMessagesModel.swift | 2 +- DuckDuckGo/NewTabPageView.swift | 9 +- DuckDuckGo/NewTabPageViewController.swift | 4 +- DuckDuckGo/RemoteMessagingClient.swift | 259 ++++++------------ ...RemoteMessagingConfigMatcherProvider.swift | 142 ++++++++++ .../RemoteMessagingSurveyURLBuilder.swift | 138 ---------- DuckDuckGoTests/MockDependencyProvider.swift | 4 - 19 files changed, 306 insertions(+), 363 deletions(-) create mode 100644 DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift delete mode 100644 DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift diff --git a/Core/AppConfigurationURLProvider.swift b/Core/AppConfigurationURLProvider.swift index 7481d8eb93..262b066655 100644 --- a/Core/AppConfigurationURLProvider.swift +++ b/Core/AppConfigurationURLProvider.swift @@ -32,6 +32,7 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { case .trackerDataSet: return URL.trackerDataSet case .surrogates: return URL.surrogates case .FBConfig: fatalError("This feature is not supported on iOS") + case .remoteMessagingConfig: return RemoteMessagingClient.Constants.endpoint } } diff --git a/Core/Configuration.swift b/Core/Configuration.swift index e7c9dcb631..513389febf 100644 --- a/Core/Configuration.swift +++ b/Core/Configuration.swift @@ -31,6 +31,7 @@ public extension Configuration { case .surrogates: return "surrogates" case .trackerDataSet: return "trackerDataSet" case .FBConfig: return "FBConfig" + case .remoteMessagingConfig: return "remoteMessagingConfig" } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 57fc9a1480..01a7899b30 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -184,6 +184,7 @@ 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F962A155F7C0029F789 /* SyncDataProviders.swift */; }; 3760DFED299315EF0045A446 /* Waitlist in Frameworks */ = {isa = PBXBuildFile; productRef = 3760DFEC299315EF0045A446 /* Waitlist */; }; + 3768D8472C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3768D8462C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift */; }; 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; 37A6A8FE2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8FD2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift */; }; @@ -217,7 +218,6 @@ 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */; }; 4B6ED9452B992FE4007F5CAA /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */; }; 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */; }; - 4B78074E2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */; }; 4B948E2629DCCDB9002531FA /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 4B948E2529DCCDB9002531FA /* Persistence */; }; 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */; }; 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BBBBA862B02E85400D965DA /* DesignResourcesKit */; }; @@ -1305,6 +1305,7 @@ 3736088F2ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeStorage.swift; sourceTree = ""; }; 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoritesDisplayMode+UserDefaults.swift"; sourceTree = ""; }; 37445F962A155F7C0029F789 /* SyncDataProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataProviders.swift; sourceTree = ""; }; + 3768D8462C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingConfigMatcherProvider.swift; sourceTree = ""; }; 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeSyncHandler.swift; sourceTree = ""; }; 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCleanupErrorHandling.swift; sourceTree = ""; }; 37A6A8FD2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboarding.swift; sourceTree = ""; }; @@ -1335,7 +1336,6 @@ 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuControllerView.swift; sourceTree = ""; }; 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintingUserScript.swift; sourceTree = ""; }; - 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingSurveyURLBuilder.swift; sourceTree = ""; }; 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWidget.swift; sourceTree = ""; }; 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistUserText.swift; sourceTree = ""; }; 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; @@ -3355,7 +3355,6 @@ isa = PBXGroup; children = ( 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */, - 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */, BDFF031F2BA3D3AD00F324C9 /* Feature Visibility */, ); name = VPN; @@ -4421,6 +4420,7 @@ isa = PBXGroup; children = ( C1B7B52128941F2A0098FD6A /* RemoteMessagingClient.swift */, + 3768D8462C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift */, 3712091D2C21E390003ADF3D /* RemoteMessagingStoreErrorHandling.swift */, ); name = RemoteMessaging; @@ -6522,6 +6522,7 @@ B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, + 3768D8472C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */, 1D200C992BA3176D00108701 /* SettingsOthersView.swift in Sources */, 31DD208427395A5A008FB313 /* VoiceSearchHelper.swift in Sources */, 9874F9EE2187AFCE00CAF33D /* Themable.swift in Sources */, @@ -6576,7 +6577,6 @@ F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */, 37CF91602BB4737300BADCAE /* CrashCollectionOnboarding.swift in Sources */, C1B924B72ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift in Sources */, - 4B78074E2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift in Sources */, 3132FA2A27A0788F00DD7A12 /* QuickLookPreviewHelper.swift in Sources */, D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */, C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */, @@ -9946,7 +9946,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 167.0.1; + version = 169.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d2d55714a..804d062cbb 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "0746af01b77d39a1e037bea93b46591534a13b5c", - "version" : "167.0.1" + "revision" : "bfabf4518a33eb2b4b11003a15633a24c28fa922", + "version" : "169.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "7ac68ae3bc052fa59adbc1ba8fd5cb5849a6bc99", - "version" : "5.25.0" + "revision" : "9c65477457126ab7ad963a32b7f85ce08e6bd1a7", + "version" : "6.0.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 92d0396ef9..3cdd86b290 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -32,6 +32,7 @@ import Crashes import Configuration import Networking import DDGSync +import RemoteMessaging import SyncDataProviders import Subscription @@ -79,6 +80,10 @@ import WebKit private var showKeyboardIfSettingOn = true private var lastBackgroundDate: Date? + private(set) var homePageConfiguration: HomePageConfiguration! + + private(set) var remoteMessagingClient: RemoteMessagingClient! + private(set) var syncService: DDGSync! private(set) var syncDataProviders: SyncDataProviders! private var syncDidFinishCancellable: AnyCancellable? @@ -278,6 +283,22 @@ import WebKit }) } + remoteMessagingClient = RemoteMessagingClient( + bookmarksDatabase: bookmarksDatabase, + appSettings: AppDependencyProvider.shared.appSettings, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + configurationStore: ConfigurationStore.shared, + database: Database.shared, + errorEvents: RemoteMessagingStoreErrorHandling(), + remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager + ) + ) + remoteMessagingClient.registerBackgroundRefreshTaskHandler() + + homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient) + let previewsSource = TabPreviewsSource() let historyManager = makeHistoryManager(AppDependencyProvider.shared.appSettings, AppDependencyProvider.shared.internalUserDecider, @@ -287,6 +308,7 @@ import WebKit let main = MainViewController(bookmarksDatabase: bookmarksDatabase, bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, historyManager: historyManager, + homePageConfiguration: homePageConfiguration, syncService: syncService, syncDataProviders: syncDataProviders, appSettings: AppDependencyProvider.shared.appSettings, @@ -318,11 +340,6 @@ import WebKit // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. AppConfigurationFetch.registerBackgroundRefreshTaskHandler() - RemoteMessagingClient.registerBackgroundRefreshTaskHandler( - bookmarksDatabase: bookmarksDatabase, - favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode - ) - UNUserNotificationCenter.current().delegate = self window?.windowScene?.screenshotService?.delegate = self @@ -616,10 +633,7 @@ import WebKit private func refreshRemoteMessages() { Task { - try? await RemoteMessagingClient.fetchAndProcess( - bookmarksDatabase: self.bookmarksDatabase, - favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode - ) + try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) } } diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 6ccf795c6e..6cc6b5c117 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -33,8 +33,6 @@ protocol DependencyProvider { var variantManager: VariantManager { get } var internalUserDecider: InternalUserDecider { get } var featureFlagger: FeatureFlagger { get } - var remoteMessagingStore: RemoteMessagingStore { get } - var homePageConfiguration: HomePageConfiguration { get } var storageCache: StorageCache { get } var voiceSearchHelper: VoiceSearchHelperProtocol { get } var downloadManager: DownloadManager { get } @@ -64,13 +62,6 @@ class AppDependencyProvider: DependencyProvider { let internalUserDecider: InternalUserDecider = ContentBlocking.shared.privacyConfigurationManager.internalUserDecider let featureFlagger: FeatureFlagger - let remoteMessagingStore: RemoteMessagingStore = RemoteMessagingStore( - database: Database.shared, - errorEvents: RemoteMessagingStoreErrorHandling(), - log: .remoteMessaging - ) - lazy var homePageConfiguration: HomePageConfiguration = HomePageConfiguration(variantManager: variantManager, - remoteMessagingStore: remoteMessagingStore) let storageCache = StorageCache() let voiceSearchHelper: VoiceSearchHelperProtocol = VoiceSearchHelper() let downloadManager = DownloadManager() diff --git a/DuckDuckGo/ConfigurationDebugViewController.swift b/DuckDuckGo/ConfigurationDebugViewController.swift index 15d7c25392..1c8afce50a 100644 --- a/DuckDuckGo/ConfigurationDebugViewController.swift +++ b/DuckDuckGo/ConfigurationDebugViewController.swift @@ -54,6 +54,7 @@ class ConfigurationDebugViewController: UITableViewController { case surrogates case trackerDataSet case privacyConfiguration + case remoteMessagingConfig case resetEtags = "Reset ETags" var showDetail: Bool { diff --git a/DuckDuckGo/ConfigurationURLDebugViewController.swift b/DuckDuckGo/ConfigurationURLDebugViewController.swift index 8de0d0aa2f..bd217e3940 100644 --- a/DuckDuckGo/ConfigurationURLDebugViewController.swift +++ b/DuckDuckGo/ConfigurationURLDebugViewController.swift @@ -180,6 +180,7 @@ struct CustomConfigurationURLProvider: ConfigurationURLProviding { var customTrackerDataSetURL: URL? var customSurrogatesURL: URL? var customFBConfigURL: URL? + var customRemoteMessagingConfigURL: URL? let defaultProvider = AppConfigurationURLProvider() @@ -194,6 +195,7 @@ struct CustomConfigurationURLProvider: ConfigurationURLProviding { case .trackerDataSet: customURL = customTrackerDataSetURL case .surrogates: customURL = customSurrogatesURL case .FBConfig: customURL = nil + case .remoteMessagingConfig: customURL = customRemoteMessagingConfigURL } return customURL ?? defaultURL } diff --git a/DuckDuckGo/HomeCollectionView.swift b/DuckDuckGo/HomeCollectionView.swift index 729b30f92e..9d29f3e9e3 100644 --- a/DuckDuckGo/HomeCollectionView.swift +++ b/DuckDuckGo/HomeCollectionView.swift @@ -27,15 +27,14 @@ class HomeCollectionView: UICollectionView { static let topInset: CGFloat = 79 } + var homePageConfiguration: HomePageConfiguration! private weak var controller: HomeViewController! private(set) var renderers: HomeViewSectionRenderers! private lazy var collectionViewReorderingGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.collectionViewReorderingGestureHandler(gesture:))) - - private lazy var homePageConfiguration = AppDependencyProvider.shared.homePageConfiguration - + private var topIndexPath: IndexPath? { for section in 0.. 0 { return IndexPath(row: 0, section: section) diff --git a/DuckDuckGo/HomePageConfiguration.swift b/DuckDuckGo/HomePageConfiguration.swift index b7832ef2a3..14aa2c37d1 100644 --- a/DuckDuckGo/HomePageConfiguration.swift +++ b/DuckDuckGo/HomePageConfiguration.swift @@ -44,14 +44,13 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { // MARK: - Messages private var homeMessageStorage: HomeMessageStorage - private var remoteMessagingStore: RemoteMessagingStore + private var remoteMessagingClient: RemoteMessagingClient var homeMessages: [HomeMessage] = [] - init(variantManager: VariantManager? = nil, - remoteMessagingStore: RemoteMessagingStore = AppDependencyProvider.shared.remoteMessagingStore) { + init(variantManager: VariantManager? = nil, remoteMessagingClient: RemoteMessagingClient) { homeMessageStorage = HomeMessageStorage(variantManager: variantManager) - self.remoteMessagingStore = remoteMessagingStore + self.remoteMessagingClient = remoteMessagingClient homeMessages = buildHomeMessages() } @@ -75,7 +74,7 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { } private func remoteMessageToShow() -> HomeMessage? { - guard let remoteMessageToPresent = remoteMessagingStore.fetchScheduledRemoteMessage() else { return nil } + guard let remoteMessageToPresent = remoteMessagingClient.store.fetchScheduledRemoteMessage() else { return nil } os_log("Remote message to show: %s", log: .remoteMessaging, type: .info, remoteMessageToPresent.id) return .remoteMessage(remoteMessage: remoteMessageToPresent) } @@ -84,7 +83,7 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { switch homeMessage { case .remoteMessage(let remoteMessage): os_log("Home message dismissed: %s", log: .remoteMessaging, type: .info, remoteMessage.id) - remoteMessagingStore.dismissRemoteMessage(withId: remoteMessage.id) + remoteMessagingClient.store.dismissRemoteMessage(withID: remoteMessage.id) if let index = homeMessages.firstIndex(of: homeMessage) { homeMessages.remove(at: index) @@ -102,11 +101,11 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { Pixel.fire(pixel: .remoteMessageShown, withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) - if !remoteMessagingStore.hasShownRemoteMessage(withId: remoteMessage.id) { + if !remoteMessagingClient.store.hasShownRemoteMessage(withID: remoteMessage.id) { os_log("Remote message shown for first time: %s", log: .remoteMessaging, type: .info, remoteMessage.id) Pixel.fire(pixel: .remoteMessageShownUnique, withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) - remoteMessagingStore.updateRemoteMessage(withId: remoteMessage.id, asShown: true) + remoteMessagingClient.store.updateRemoteMessage(withID: remoteMessage.id, asShown: true) } default: diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index ffe182474e..dad56cca12 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -68,6 +68,7 @@ class HomeViewController: UIViewController, NewTabPage { private var viewHasAppeared = false private var defaultVerticalAlignConstant: CGFloat = 0 + private let homePageConfiguration: HomePageConfiguration private let tabModel: Tab private let favoritesViewModel: FavoritesListInteracting private let appSettings: AppSettings @@ -76,7 +77,9 @@ class HomeViewController: UIViewController, NewTabPage { private var viewModelCancellable: AnyCancellable? private var favoritesDisplayModeCancellable: AnyCancellable? + // swiftlint:disable:next function_parameter_count static func loadFromStoryboard( + homePageConfiguration: HomePageConfiguration, model: Tab, favoritesViewModel: FavoritesListInteracting, appSettings: AppSettings, @@ -87,6 +90,7 @@ class HomeViewController: UIViewController, NewTabPage { let controller = storyboard.instantiateViewController(identifier: "HomeViewController", creator: { coder in HomeViewController( coder: coder, + homePageConfiguration: homePageConfiguration, tabModel: model, favoritesViewModel: favoritesViewModel, appSettings: appSettings, @@ -99,12 +103,14 @@ class HomeViewController: UIViewController, NewTabPage { required init?( coder: NSCoder, + homePageConfiguration: HomePageConfiguration, tabModel: Tab, favoritesViewModel: FavoritesListInteracting, appSettings: AppSettings, syncService: DDGSyncing, syncDataProviders: SyncDataProviders ) { + self.homePageConfiguration = homePageConfiguration self.tabModel = tabModel self.favoritesViewModel = favoritesViewModel self.appSettings = appSettings @@ -124,6 +130,7 @@ class HomeViewController: UIViewController, NewTabPage { NotificationCenter.default.addObserver(self, selector: #selector(HomeViewController.onKeyboardChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + collectionView.homePageConfiguration = homePageConfiguration configureCollectionView() decorate() @@ -160,9 +167,11 @@ class HomeViewController: UIViewController, NewTabPage { } @objc func remoteMessagesDidChange() { - os_log("Remote messages did change", log: .remoteMessaging, type: .info) - collectionView.refreshHomeConfiguration() - refresh() + DispatchQueue.main.async { + os_log("Remote messages did change", log: .remoteMessaging, type: .info) + self.collectionView.refreshHomeConfiguration() + self.refresh() + } } func configureCollectionView() { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 422e2eac4c..f125b722da 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -90,6 +90,7 @@ class MainViewController: UIViewController { var tabsBarController: TabsBarViewController? var suggestionTrayController: SuggestionTrayViewController? + let homePageConfiguration: HomePageConfiguration let homeTabManager: NewTabPageManager let tabManager: TabManager let previewsSource: TabPreviewsSource @@ -177,6 +178,7 @@ class MainViewController: UIViewController { bookmarksDatabase: CoreDataDatabase, bookmarksDatabaseCleaner: BookmarkDatabaseCleaner, historyManager: HistoryManager, + homePageConfiguration: HomePageConfiguration, syncService: DDGSyncing, syncDataProviders: SyncDataProviders, appSettings: AppSettings, @@ -187,6 +189,7 @@ class MainViewController: UIViewController { self.bookmarksDatabase = bookmarksDatabase self.bookmarksDatabaseCleaner = bookmarksDatabaseCleaner self.historyManager = historyManager + self.homePageConfiguration = homePageConfiguration self.syncService = syncService self.syncDataProviders = syncDataProviders self.favoritesViewModel = FavoritesListViewModel(bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: appSettings.favoritesDisplayMode) @@ -678,7 +681,7 @@ class MainViewController: UIViewController { private func addLaunchTabNotificationObserver() { launchTabObserver = LaunchTabNotification.addObserver(handler: { [weak self] urlString in guard let self = self else { return } - if let url = URL(string: urlString) { + if let url = URL(trimmedAddressBarString: urlString), url.isValid { self.loadUrlInNewTab(url, inheritedAttribution: nil) } else { self.loadQuery(urlString) @@ -728,7 +731,7 @@ class MainViewController: UIViewController { currentTab?.dismiss() removeHomeScreen() - AppDependencyProvider.shared.homePageConfiguration.refresh() + homePageConfiguration.refresh() // Access the tab model directly as we don't want to create a new tab controller here guard let tabModel = tabManager.model.currentTab else { @@ -736,12 +739,13 @@ class MainViewController: UIViewController { } if homeTabManager.isNewTabPageSectionsEnabled { - let controller = NewTabPageViewController() + let controller = NewTabPageViewController(homePageMessagesConfiguration: homePageConfiguration) newTabPageViewController = controller addToContentContainer(controller: controller) viewCoordinator.logoContainer.isHidden = true } else { - let controller = HomeViewController.loadFromStoryboard(model: tabModel, + let controller = HomeViewController.loadFromStoryboard(homePageConfiguration: homePageConfiguration, + model: tabModel, favoritesViewModel: favoritesViewModel, appSettings: appSettings, syncService: syncService, diff --git a/DuckDuckGo/NewTabPageMessagesModel.swift b/DuckDuckGo/NewTabPageMessagesModel.swift index 7f6ec3e60a..486790bd58 100644 --- a/DuckDuckGo/NewTabPageMessagesModel.swift +++ b/DuckDuckGo/NewTabPageMessagesModel.swift @@ -31,7 +31,7 @@ final class NewTabPageMessagesModel: ObservableObject { private let notificationCenter: NotificationCenter private let pixelFiring: PixelFiring.Type - init(homePageMessagesConfiguration: HomePageMessagesConfiguration = AppDependencyProvider.shared.homePageConfiguration, + init(homePageMessagesConfiguration: HomePageMessagesConfiguration, notificationCenter: NotificationCenter = .default, pixelFiring: PixelFiring.Type = Pixel.self) { self.homePageMessagesConfiguration = homePageMessagesConfiguration diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index fcb7e5907b..e0bb708ab8 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -81,7 +81,14 @@ struct NewTabPageView: View { // MARK: - Preview #Preview("Regular") { - NewTabPageView(messagesModel: NewTabPageMessagesModel(), favoritesModel: FavoritesModel()) + NewTabPageView( + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesModel: FavoritesModel() + ) } #Preview("With message") { diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index b44b93a78e..f8f66c5113 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -21,8 +21,8 @@ import SwiftUI final class NewTabPageViewController: UIHostingController, NewTabPage { - init() { - let newTabPageView = NewTabPageView(messagesModel: NewTabPageMessagesModel(), + init(homePageMessagesConfiguration: HomePageMessagesConfiguration) { + let newTabPageView = NewTabPageView(messagesModel: NewTabPageMessagesModel(homePageMessagesConfiguration: homePageMessagesConfiguration), favoritesModel: FavoritesModel()) super.init(rootView: newTabPageView) } diff --git a/DuckDuckGo/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessagingClient.swift index 0925657a2d..da01ddd6ef 100644 --- a/DuckDuckGo/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessagingClient.swift @@ -18,6 +18,7 @@ // import Common +import Configuration import Foundation import Core import BackgroundTasks @@ -25,42 +26,104 @@ import BrowserServicesKit import Persistence import Bookmarks import RemoteMessaging -import NetworkProtection -import Subscription -struct RemoteMessagingClient { +final class RemoteMessagingClient: RemoteMessagingProcessing { - private static let endpoint: URL = { + struct Constants { + static let backgroundRefreshTaskIdentifier = "com.duckduckgo.app.remoteMessageRefresh" + static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 60 * 4 + static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! #else - URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/ios-config.json")! + URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/ios-config.json")! #endif - }() + }() + } + + let endpoint: URL = Constants.endpoint + let configFetcher: RemoteMessagingConfigFetching + let configMatcherProvider: RemoteMessagingConfigMatcherProviding + let store: RemoteMessagingStoring + let remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + + convenience init( + bookmarksDatabase: CoreDataDatabase, + appSettings: AppSettings, + internalUserDecider: InternalUserDecider, + configurationStore: ConfigurationStoring, + database: CoreDataDatabase, + notificationCenter: NotificationCenter = .default, + errorEvents: EventMapping?, + remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + ) { + let provider = RemoteMessagingConfigMatcherProvider( + bookmarksDatabase: bookmarksDatabase, + appSettings: appSettings, + internalUserDecider: internalUserDecider + ) + let configFetcher = RemoteMessagingConfigFetcher( + configurationFetcher: ConfigurationFetcher(store: configurationStore, urlSession: .session(), log: .remoteMessaging, eventMapping: nil), + configurationStore: configurationStore + ) + let remoteMessagingStore = RemoteMessagingStore( + database: database, + notificationCenter: notificationCenter, + errorEvents: errorEvents, + remoteMessagingAvailabilityProvider: remoteMessagingAvailabilityProvider, + log: .remoteMessaging + ) + self.init( + configMatcherProvider: provider, + configFetcher: configFetcher, + store: remoteMessagingStore, + remoteMessagingAvailabilityProvider: remoteMessagingAvailabilityProvider + ) + } + + init( + configMatcherProvider: RemoteMessagingConfigMatcherProviding, + configFetcher: RemoteMessagingConfigFetching, + store: RemoteMessagingStoring, + remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + ) { + self.configMatcherProvider = configMatcherProvider + self.configFetcher = configFetcher + self.store = store + self.remoteMessagingAvailabilityProvider = remoteMessagingAvailabilityProvider + } @UserDefaultsWrapper(key: .lastRemoteMessagingRefreshDate, defaultValue: .distantPast) static private var lastRemoteMessagingRefreshDate: Date +} - struct Constants { - static let backgroundRefreshTaskIdentifier = "com.duckduckgo.app.remoteMessageRefresh" - static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 60 * 4 - } +// MARK: - Background Refresh + +extension RemoteMessagingClient { static private var shouldRefresh: Bool { - return Date().timeIntervalSince(Self.lastRemoteMessagingRefreshDate) > Constants.minimumConfigurationRefreshInterval + Date().timeIntervalSince(Self.lastRemoteMessagingRefreshDate) > Constants.minimumConfigurationRefreshInterval } - static func registerBackgroundRefreshTaskHandler( - bookmarksDatabase: CoreDataDatabase, - favoritesDisplayMode: @escaping @autoclosure () -> FavoritesDisplayMode - ) { + func registerBackgroundRefreshTaskHandler() { + let provider = configMatcherProvider + let fetcher = configFetcher + let remoteMessagingAvailabilityProvider = remoteMessagingAvailabilityProvider + let store = store + BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundRefreshTaskIdentifier, using: nil) { task in - guard shouldRefresh else { + guard Self.shouldRefresh else { task.setTaskCompleted(success: true) - scheduleBackgroundRefreshTask() + Self.scheduleBackgroundRefreshTask() return } - backgroundRefreshTaskHandler(bgTask: task, bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: favoritesDisplayMode()) + let client = RemoteMessagingClient( + configMatcherProvider: provider, + configFetcher: fetcher, + store: store, + remoteMessagingAvailabilityProvider: remoteMessagingAvailabilityProvider + ) + Self.backgroundRefreshTaskHandler(bgTask: task, client: client) } } @@ -86,15 +149,13 @@ struct RemoteMessagingClient { #endif } - static func backgroundRefreshTaskHandler( - bgTask: BGTask, - bookmarksDatabase: CoreDataDatabase, - favoritesDisplayMode: @escaping @autoclosure () -> FavoritesDisplayMode - ) { + static func backgroundRefreshTaskHandler(bgTask: BGTask, client: RemoteMessagingClient) { let fetchAndProcessTask = Task { do { - try await Self.fetchAndProcess(bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: favoritesDisplayMode()) - Self.lastRemoteMessagingRefreshDate = Date() + if client.remoteMessagingAvailabilityProvider.isRemoteMessagingAvailable { + try await client.fetchAndProcess(using: client.store) + Self.lastRemoteMessagingRefreshDate = Date() + } scheduleBackgroundRefreshTask() bgTask.setTaskCompleted(success: true) } catch { @@ -108,150 +169,4 @@ struct RemoteMessagingClient { bgTask.setTaskCompleted(success: false) } } - - /// Convenience function - static func fetchAndProcess(bookmarksDatabase: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode) async throws { - - var bookmarksCount = 0 - var favoritesCount = 0 - let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) - context.performAndWait { - let displayedFavoritesFolder = BookmarkUtils.fetchFavoritesFolder(withUUID: favoritesDisplayMode.displayedFolder.rawValue, in: context)! - - let bookmarksCountRequest = BookmarkEntity.fetchRequest() - bookmarksCountRequest.predicate = NSPredicate( - format: "SUBQUERY(%K, $x, $x CONTAINS %@).@count == 0 AND %K == false AND %K == false AND (%K == NO OR %K == nil)", - #keyPath(BookmarkEntity.favoriteFolders), - displayedFavoritesFolder, - #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.isPendingDeletion), - #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) - bookmarksCount = (try? context.count(for: bookmarksCountRequest)) ?? 0 - - let favoritesCountRequest = BookmarkEntity.fetchRequest() - favoritesCountRequest.predicate = NSPredicate(format: "%K CONTAINS %@ AND %K == false AND %K == false AND (%K == NO OR %K == nil)", - #keyPath(BookmarkEntity.favoriteFolders), - displayedFavoritesFolder, - #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.isPendingDeletion), - #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) - favoritesCount = (try? context.count(for: favoritesCountRequest)) ?? 0 - } - - let isWidgetInstalled = await AppDependencyProvider.shared.appSettings.isWidgetInstalled() - - try await Self.fetchAndProcess(bookmarksCount: bookmarksCount, - favoritesCount: favoritesCount, - isWidgetInstalled: isWidgetInstalled) - } - - // swiftlint:disable:next function_body_length - static private func fetchAndProcess(bookmarksCount: Int, - favoritesCount: Int, - remoteMessagingStore: RemoteMessagingStore = AppDependencyProvider.shared.remoteMessagingStore, - statisticsStore: StatisticsStore = StatisticsUserDefaults(), - variantManager: VariantManager = DefaultVariantManager(), - isWidgetInstalled: Bool) async throws { - - let result = await Self.fetchRemoteMessages(remoteMessageRequest: RemoteMessageRequest(endpoint: endpoint)) - - switch result { - case .success(let statusResponse): - os_log("Successfully fetched remote messages", log: .remoteMessaging, type: .debug) - - let isPrivacyProSubscriber = AppDependencyProvider.shared.subscriptionManager.accountManager.isUserAuthenticated - let canPurchase = AppDependencyProvider.shared.subscriptionManager.canPurchase - - let activationDateStore = DefaultVPNActivationDateStore() - let daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1 - - var privacyProDaysSinceSubscribed: Int = -1 - var privacyProDaysUntilExpiry: Int = -1 - var privacyProPurchasePlatform: String? - var privacyProIsActive: Bool = false - var privacyProIsExpiring: Bool = false - var privacyProIsExpired: Bool = false - let surveyActionMapper: DefaultRemoteMessagingSurveyURLBuilder - - if let accessToken = AppDependencyProvider.shared.subscriptionManager.accountManager.accessToken { - let subscriptionResult = await AppDependencyProvider.shared.subscriptionManager.subscriptionEndpointService.getSubscription( - accessToken: accessToken - ) - - if case let .success(subscription) = subscriptionResult { - privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 - privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 - privacyProPurchasePlatform = subscription.platform.rawValue - - switch subscription.status { - case .autoRenewable, .gracePeriod: - privacyProIsActive = true - case .notAutoRenewable: - privacyProIsExpiring = true - case .expired, .inactive: - privacyProIsExpired = true - case .unknown: - break // Not supported in RMF - } - - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, subscription: subscription) - } else { - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, subscription: nil) - } - } else { - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, subscription: nil) - } - - let dismissedMessageIds = remoteMessagingStore.fetchDismissedRemoteMessageIds() - - let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, - variantManager: variantManager, - isInternalUser: AppDependencyProvider.shared.internalUserDecider.isInternalUser), - userAttributeMatcher: UserAttributeMatcher(statisticsStore: statisticsStore, - variantManager: variantManager, - bookmarksCount: bookmarksCount, - favoritesCount: favoritesCount, - appTheme: AppUserDefaults().currentThemeName.rawValue, - isWidgetInstalled: isWidgetInstalled, - daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled, - isPrivacyProEligibleUser: canPurchase, - isPrivacyProSubscriber: isPrivacyProSubscriber, - privacyProDaysSinceSubscribed: privacyProDaysSinceSubscribed, - privacyProDaysUntilExpiry: privacyProDaysUntilExpiry, - privacyProPurchasePlatform: privacyProPurchasePlatform, - isPrivacyProSubscriptionActive: privacyProIsActive, - isPrivacyProSubscriptionExpiring: privacyProIsExpiring, - isPrivacyProSubscriptionExpired: privacyProIsExpired, - dismissedMessageIds: dismissedMessageIds), - percentileStore: RemoteMessagingPercentileUserDefaultsStore(userDefaults: .standard), - surveyActionMapper: surveyActionMapper, - dismissedMessageIds: dismissedMessageIds - ) - - let processor = RemoteMessagingConfigProcessor(remoteMessagingConfigMatcher: remoteMessagingConfigMatcher) - let config = remoteMessagingStore.fetchRemoteMessagingConfig() - - if let processorResult = processor.process(jsonRemoteMessagingConfig: statusResponse, - currentConfig: config) { - remoteMessagingStore.saveProcessedResult(processorResult) - } - case .failure(let error): - os_log("Failed to fetch remote messages", log: .remoteMessaging, type: .error) - throw error - } - } - - static func fetchRemoteMessages(remoteMessageRequest: RemoteMessageRequest) async -> Result { - return await withCheckedContinuation { continuation in - remoteMessageRequest.getRemoteMessage(completionHandler: { result in - switch result { - case .success(let response): - continuation.resume(returning: .success(response)) - case .failure(let error): - continuation.resume(returning: .failure(error)) - } - }) - } - } } diff --git a/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift new file mode 100644 index 0000000000..ef47b85c6c --- /dev/null +++ b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift @@ -0,0 +1,142 @@ +// +// RemoteMessagingConfigMatcherProvider.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import Core +import Foundation +import BrowserServicesKit +import Persistence +import Bookmarks +import RemoteMessaging +import NetworkProtection +import Subscription + +extension DefaultVPNActivationDateStore: VPNActivationDateProviding {} + +final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherProviding { + + init( + bookmarksDatabase: CoreDataDatabase, + appSettings: AppSettings, + internalUserDecider: InternalUserDecider + ) { + self.bookmarksDatabase = bookmarksDatabase + self.appSettings = appSettings + self.internalUserDecider = internalUserDecider + } + + let bookmarksDatabase: CoreDataDatabase + let appSettings: AppSettings + let internalUserDecider: InternalUserDecider + + // swiftlint:disable:next function_body_length + func refreshConfigMatcher(using store: RemoteMessagingStoring) async -> RemoteMessagingConfigMatcher { + + var bookmarksCount = 0 + var favoritesCount = 0 + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + context.performAndWait { + bookmarksCount = BookmarkUtils.numberOfBookmarks(in: context) + favoritesCount = BookmarkUtils.numberOfFavorites(for: appSettings.favoritesDisplayMode, in: context) + } + + let statisticsStore = StatisticsUserDefaults() + let variantManager = DefaultVariantManager() + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager + + let isPrivacyProSubscriber = subscriptionManager.accountManager.isUserAuthenticated + let isPrivacyProEligibleUser = subscriptionManager.canPurchase + + let activationDateStore = DefaultVPNActivationDateStore() + let daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1 + + var privacyProDaysSinceSubscribed: Int = -1 + var privacyProDaysUntilExpiry: Int = -1 + var privacyProPurchasePlatform: String? + var isPrivacyProSubscriptionActive: Bool = false + var isPrivacyProSubscriptionExpiring: Bool = false + var isPrivacyProSubscriptionExpired: Bool = false + let surveyActionMapper: DefaultRemoteMessagingSurveyURLBuilder + + if let accessToken = subscriptionManager.accountManager.accessToken { + let subscriptionResult = await subscriptionManager.subscriptionEndpointService.getSubscription( + accessToken: accessToken + ) + + if case let .success(subscription) = subscriptionResult { + privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 + privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 + privacyProPurchasePlatform = subscription.platform.rawValue + + switch subscription.status { + case .autoRenewable, .gracePeriod: + isPrivacyProSubscriptionActive = true + case .notAutoRenewable: + isPrivacyProSubscriptionExpiring = true + case .expired, .inactive: + isPrivacyProSubscriptionExpired = true + case .unknown: + break // Not supported in RMF + } + + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( + statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: subscription) + } else { + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( + statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) + } + } else { + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( + statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) + } + + let dismissedMessageIds = store.fetchDismissedRemoteMessageIDs() + + return RemoteMessagingConfigMatcher( + appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, + variantManager: variantManager, + isInternalUser: internalUserDecider.isInternalUser), + userAttributeMatcher: UserAttributeMatcher(statisticsStore: statisticsStore, + variantManager: variantManager, + bookmarksCount: bookmarksCount, + favoritesCount: favoritesCount, + appTheme: appSettings.currentThemeName.rawValue, + isWidgetInstalled: await appSettings.isWidgetInstalled(), + daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled, + isPrivacyProEligibleUser: isPrivacyProEligibleUser, + isPrivacyProSubscriber: isPrivacyProSubscriber, + privacyProDaysSinceSubscribed: privacyProDaysSinceSubscribed, + privacyProDaysUntilExpiry: privacyProDaysUntilExpiry, + privacyProPurchasePlatform: privacyProPurchasePlatform, + isPrivacyProSubscriptionActive: isPrivacyProSubscriptionActive, + isPrivacyProSubscriptionExpiring: isPrivacyProSubscriptionExpiring, + isPrivacyProSubscriptionExpired: isPrivacyProSubscriptionExpired, + dismissedMessageIds: dismissedMessageIds), + percentileStore: RemoteMessagingPercentileUserDefaultsStore(keyValueStore: UserDefaults.standard), + surveyActionMapper: surveyActionMapper, + dismissedMessageIds: dismissedMessageIds + ) + } +} diff --git a/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift b/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift deleted file mode 100644 index 1ea51ddc39..0000000000 --- a/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// RemoteMessagingSurveyURLBuilder.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import BrowserServicesKit -import RemoteMessaging -import Core -import Common -import Subscription - -struct DefaultRemoteMessagingSurveyURLBuilder: RemoteMessagingSurveyActionMapping { - - private let statisticsStore: StatisticsStore - private let vpnActivationDateStore: VPNActivationDateStore - private let subscription: Subscription? - - init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), - vpnActivationDateStore: VPNActivationDateStore = DefaultVPNActivationDateStore(), - subscription: Subscription?) { - self.statisticsStore = statisticsStore - self.vpnActivationDateStore = vpnActivationDateStore - self.subscription = subscription - } - - // swiftlint:disable:next cyclomatic_complexity function_body_length - func add(parameters: [RemoteMessagingSurveyActionParameter], to surveyURL: URL) -> URL { - guard var components = URLComponents(string: surveyURL.absoluteString) else { - assertionFailure("Could not build URL components from survey URL") - return surveyURL - } - - var queryItems = components.queryItems ?? [] - - for parameter in parameters { - switch parameter { - case .atb: - if let atb = statisticsStore.atb { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: atb)) - } - case .atbVariant: - if let variant = statisticsStore.variant { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: variant)) - } - case .osVersion: - queryItems.append(URLQueryItem(name: parameter.rawValue, value: AppVersion.shared.osVersion)) - case .appVersion: - queryItems.append(URLQueryItem(name: parameter.rawValue, value: AppVersion.shared.versionAndBuildNumber)) - case .hardwareModel: - let model = hardwareModel().addingPercentEncoding(withAllowedCharacters: .alphanumerics) - queryItems.append(URLQueryItem(name: parameter.rawValue, value: model)) - case .daysInstalled: - if let installDate = statisticsStore.installDate, - let daysSinceInstall = Calendar.current.numberOfDaysBetween(installDate, and: Date()) { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSinceInstall))) - } - case .privacyProStatus: - switch subscription?.status { - case .autoRenewable: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "auto_renewable")) - case .notAutoRenewable: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "not_auto_renewable")) - case .gracePeriod: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "grace_period")) - case .inactive: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "inactive")) - case .expired: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "expired")) - case .unknown: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "unknown")) - case nil: break - } - case .privacyProPlatform: - - switch subscription?.platform { - case .apple: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "apple")) - case .google: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "google")) - case .stripe: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "stripe")) - case .unknown: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "unknown")) - case nil: break - } - case .privacyProBilling: - switch subscription?.billingPeriod { - case .monthly: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "monthly")) - case .yearly: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "yearly")) - case .unknown: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "unknown")) - case nil: break - } - - case .privacyProDaysSincePurchase: - if let startDate = subscription?.startedAt, - let daysSincePurchase = Calendar.current.numberOfDaysBetween(startDate, and: Date()) { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSincePurchase))) - } - case .privacyProDaysUntilExpiry: - if let expiryDate = subscription?.expiresOrRenewsAt, - let daysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: expiryDate) { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysUntilExpiry))) - } - case .vpnFirstUsed: - if let vpnFirstUsed = vpnActivationDateStore.daysSinceActivation() { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: vpnFirstUsed))) - } - case .vpnLastUsed: - if let vpnLastUsed = vpnActivationDateStore.daysSinceLastActive() { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: vpnLastUsed))) - } - } - } - - components.queryItems = queryItems - - return components.url ?? surveyURL - } - - private func hardwareModel() -> String { - var systemInfo = utsname() - uname(&systemInfo) - - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } - - return identifier - } - -} diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index 68711cf742..7a0a327087 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -32,8 +32,6 @@ class MockDependencyProvider: DependencyProvider { var variantManager: VariantManager var featureFlagger: FeatureFlagger var internalUserDecider: InternalUserDecider - var remoteMessagingStore: RemoteMessagingStore - var homePageConfiguration: HomePageConfiguration var storageCache: StorageCache var voiceSearchHelper: VoiceSearchHelperProtocol var downloadManager: DownloadManager @@ -56,8 +54,6 @@ class MockDependencyProvider: DependencyProvider { variantManager = defaultProvider.variantManager featureFlagger = defaultProvider.featureFlagger internalUserDecider = defaultProvider.internalUserDecider - remoteMessagingStore = defaultProvider.remoteMessagingStore - homePageConfiguration = defaultProvider.homePageConfiguration storageCache = defaultProvider.storageCache voiceSearchHelper = defaultProvider.voiceSearchHelper downloadManager = defaultProvider.downloadManager From 0ffe4d606a93df1e5b4533bcf42a2faf5c356513 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Tue, 9 Jul 2024 07:42:11 -0500 Subject: [PATCH 04/48] Fix double search bar for queries containing 'amp' (#3057) --- DuckDuckGo/TabViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 12a6fdd025..07bab32c00 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -1528,7 +1528,9 @@ extension TabViewController: WKNavigationDelegate { // This check needs to happen before GPC checks. Otherwise the navigation type may be rewritten to `.other` // which would skip link rewrites. - if navigationAction.navigationType != .backForward && navigationAction.isTargetingMainFrame() { + if navigationAction.navigationType != .backForward, + navigationAction.isTargetingMainFrame(), + !(navigationAction.request.url?.isDuckDuckGoSearch ?? false) { let didRewriteLink = linkProtection.requestTrackingLinkRewrite(initiatingURL: webView.url, navigationAction: navigationAction, onStartExtracting: { showProgressIndicator() }, From 4dcccdad58702c449a3f32725609ffbc8ccf06d9 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 10 Jul 2024 11:49:39 +0100 Subject: [PATCH 05/48] Update download alert copy (#3047) Task/Issue URL: https://app.asana.com/0/414235014887631/1207725995191523/f Description: Improve copy when cancelling download items --- DuckDuckGo/DownloadsList.swift | 4 ++-- DuckDuckGo/UserText.swift | 10 +++++++--- DuckDuckGo/en.lproj/Localizable.strings | 7 +++++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/DownloadsList.swift b/DuckDuckGo/DownloadsList.swift index 4353e4382b..afebd5f884 100644 --- a/DuckDuckGo/DownloadsList.swift +++ b/DuckDuckGo/DownloadsList.swift @@ -205,8 +205,8 @@ extension DownloadsList { Alert( title: Text(UserText.cancelDownloadAlertTitle), message: Text(UserText.cancelDownloadAlertDescription), - primaryButton: .cancel(Text(UserText.cancelDownloadAlertResumeAction)), - secondaryButton: .destructive(Text(UserText.cancelDownloadAlertCancelAction), action: { + primaryButton: .cancel(Text(UserText.cancelDownloadAlertNoAction)), + secondaryButton: .destructive(Text(UserText.cancelDownloadAlertYesAction), action: { cancelDownload(for: row) }) ) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 612f6f2c10..45fae6038b 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -373,9 +373,13 @@ public struct UserText { public static let cancelDownloadAlertTitle = NSLocalizedString("downloads.cancel-download.alert.title", value: "Cancel download?", comment: "Title for alert when trying to cancel the file download") public static let cancelDownloadAlertDescription = NSLocalizedString("downloads.cancel-download.alert.message", value: "Are you sure you want to cancel this download?", comment: "Message for alert when trying to cancel the file download") - public static let cancelDownloadAlertResumeAction = NSLocalizedString("downloads.cancel-download.alert.resume", value: "Resume", comment: "Resume download action for alert when trying to cancel the file download") - public static let cancelDownloadAlertCancelAction = NSLocalizedString("downloads.cancel-download.alert.cancel", value: "Cancel", comment: "Cancel download action for alert when trying to cancel the file download") - + + public static let cancelDownloadAlertCancelAction = NSLocalizedString("downloads.cancel-download.alert.cancel", value: "Cancel", comment: "Cancel download action for downloads") + public static let cancelDownloadAlertNoAction = NSLocalizedString("downloads.cancel-download.alert.no", value: "No", comment: "Confirm action for alert when trying to cancel the file download") + public static let cancelDownloadAlertYesAction = NSLocalizedString("downloads.cancel-download.alert.yes", value: "Yes", comment: "Confirm action for for alert when trying to cancel the file download") + + + public static let downloadsListDeleteAllButton = NSLocalizedString("downloads.downloads-list.delete-all", value: "Delete All", comment: "Button for deleting all items on downloads list") public static let messageDownloadFailed = NSLocalizedString("downloads.message.download-failed", value: "Failed to download. Check internet connection.", comment: "Message informing that the download has failed due to connection issues") public static let fireButtonInterruptingDownloadsAlertDescription = NSLocalizedString("downloads.fire-button.alert.message", value: "This will also cancel downloads in progress", comment: "Additional alert message shown when there are active downloads when using the fire button") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index d9900a1244..354b38f728 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -832,12 +832,15 @@ /* Message for alert when trying to cancel the file download */ "downloads.cancel-download.alert.message" = "Are you sure you want to cancel this download?"; -/* Resume download action for alert when trying to cancel the file download */ -"downloads.cancel-download.alert.resume" = "Resume"; +/* Confirm action for alert when trying to cancel the file download */ +"downloads.cancel-download.alert.no" = "No"; /* Title for alert when trying to cancel the file download */ "downloads.cancel-download.alert.title" = "Cancel download?"; +/* Confirm action for for alert when trying to cancel the file download */ +"downloads.cancel-download.alert.yes" = "Yes"; + /* Button for deleting all items on downloads list */ "downloads.downloads-list.delete-all" = "Delete All"; From 16fdcddac1d51c241dab40f58cd5ef8e7c6b1ed2 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Wed, 10 Jul 2024 12:52:33 +0100 Subject: [PATCH 06/48] Swiftlint refactoring (#3059) Task/Issue URL: https://app.asana.com/0/1205842942115003/1207743897886423/f What kind of version bump will this require?: No version bump Swiftlint rules disabled: line_length function_parameter_count function_body_length file_length type_body_length removed cyclomatic_complexity disabled only for switch cases and superfluous disable removed --- .swiftlint.yml | 21 +++++++------------ Core/AppURLs.swift | 4 ---- Core/BookmarksModelsErrorHandling.swift | 1 - Core/LegacyBookmarksStoreMigration.swift | 3 --- Core/PixelEvent.swift | 2 -- Core/UserAgentManager.swift | 5 ----- DuckDuckGo/AppDelegate+AppDeepLinks.swift | 1 - DuckDuckGo/AppDelegate.swift | 5 ----- DuckDuckGo/AppDependencyProvider.swift | 1 - DuckDuckGo/AppUserDefaults.swift | 2 -- .../Autoconsent/AutoconsentUserScript.swift | 2 -- DuckDuckGo/AutofillLoginDetailsView.swift | 5 ----- .../AutofillLoginDetailsViewModel.swift | 7 ------- DuckDuckGo/AutofillLoginListViewModel.swift | 2 -- ...ofillLoginSettingsListViewController.swift | 3 --- DuckDuckGo/BookmarksViewController.swift | 6 ------ .../ConfigurationDebugViewController.swift | 2 -- .../CrashCollectionOnboardingView.swift | 1 - DuckDuckGo/DaxDialogs.swift | 5 ----- DuckDuckGo/EmailManagerRequestDelegate.swift | 2 -- DuckDuckGo/EmailSignupViewController.swift | 8 ------- DuckDuckGo/Favicons.swift | 2 -- DuckDuckGo/FeedbackUserText.swift | 2 -- DuckDuckGo/FileSizeDebugViewController.swift | 2 +- DuckDuckGo/HomeScreenTransition.swift | 7 +------ DuckDuckGo/HomeViewController.swift | 1 - DuckDuckGo/MainViewController+Email.swift | 3 --- DuckDuckGo/MainViewController.swift | 5 ----- ...NetworkProtectionDebugViewController.swift | 8 ------- .../NetworkProtectionStatusViewModel.swift | 5 ----- DuckDuckGo/OmniBar.swift | 4 ---- .../PrivacyDashboardViewController.swift | 2 -- ...RemoteMessagingConfigMatcherProvider.swift | 1 - DuckDuckGo/SettingsViewModel.swift | 6 +----- ...scriptionPagesUseSubscriptionFeature.swift | 5 ----- .../ViewModel/SubscriptionFlowViewModel.swift | 5 +---- .../SubscriptionDebugViewController.swift | 2 -- DuckDuckGo/SyncDebugViewController.swift | 2 -- DuckDuckGo/SyncSettingsViewController.swift | 1 - DuckDuckGo/TabSwitcherViewController.swift | 2 -- DuckDuckGo/TabViewController.swift | 19 +---------------- ...bViewControllerBrowsingMenuExtension.swift | 1 - DuckDuckGo/VPNWaitlistUserText.swift | 2 -- .../AutofillLoginListViewModelTests.swift | 2 -- .../AutofillLoginPromptViewModelTests.swift | 1 - DuckDuckGoTests/BookmarksExporterTests.swift | 1 - DuckDuckGoTests/DailyPixelTests.swift | 4 ---- DuckDuckGoTests/DaxDialogTests.swift | 2 -- DuckDuckGoTests/LargeOmniBarStateTests.swift | 5 ----- DuckDuckGoTests/MockSecureVault.swift | 1 - ...kProtectionVPNLocationViewModelTests.swift | 7 ------- DuckDuckGoTests/SmallOmniBarStateTests.swift | 6 ------ DuckDuckGoTests/UserAgentTests.swift | 5 ----- FingerprintingUITests/FingerprintUITest.swift | 7 +------ .../SyncUI/Views/Internal/UserText.swift | 3 --- ...etworkProtectionPacketTunnelProvider.swift | 6 ------ PacketTunnelProvider/UserText.swift | 2 -- Widgets/WidgetViews.swift | 2 -- 58 files changed, 14 insertions(+), 215 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 9047bcb725..4abcdbfd77 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -7,7 +7,11 @@ disabled_rules: - todo - unused_capture_list - trailing_comma - - cyclomatic_complexity + - line_length + - function_parameter_count + - function_body_length + - file_length + - type_body_length opt_in_rules: - closure_end_indentation @@ -30,10 +34,12 @@ custom_rules: analyzer_rules: - unused_import +# Rule Config +cyclomatic_complexity: + ignores_case_statements: true force_cast: warning force_try: warning legacy_hashing: error - identifier_name: min_length: 1 max_length: 1000 @@ -43,29 +49,18 @@ identifier_name: - x - y - z - -line_length: - warning: 150 - ignores_urls: true - ignores_function_declarations: true - ignores_comments: true - vertical_whitespace: max_empty_lines: 2 - trailing_whitespace: ignores_empty_lines: true ignores_comments: true - private_over_fileprivate: validate_extensions: true - type_name: min_length: 3 max_length: warning: 80 error: 100 - file_header: required_pattern: | \/\/ diff --git a/Core/AppURLs.swift b/Core/AppURLs.swift index 7b4a0d3782..7851870c26 100644 --- a/Core/AppURLs.swift +++ b/Core/AppURLs.swift @@ -20,8 +20,6 @@ import BrowserServicesKit import Foundation -// swiftlint:disable line_length - public extension URL { private static let base: String = ProcessInfo.processInfo.environment["BASE_URL", default: "https://duckduckgo.com"] @@ -260,5 +258,3 @@ public final class StatisticsDependentURLFactory { } } - -// swiftlint:enable line_length diff --git a/Core/BookmarksModelsErrorHandling.swift b/Core/BookmarksModelsErrorHandling.swift index 3539714f78..3b388f4f7d 100644 --- a/Core/BookmarksModelsErrorHandling.swift +++ b/Core/BookmarksModelsErrorHandling.swift @@ -26,7 +26,6 @@ import CoreData public class BookmarksModelsErrorHandling: EventMapping { - // swiftlint:disable:next cyclomatic_complexity init(syncService: DDGSyncing? = nil) { super.init { event, error, _, _ in var domainEvent: Pixel.Event? diff --git a/Core/LegacyBookmarksStoreMigration.swift b/Core/LegacyBookmarksStoreMigration.swift index 0cb3d417ac..268a81e2e7 100644 --- a/Core/LegacyBookmarksStoreMigration.swift +++ b/Core/LegacyBookmarksStoreMigration.swift @@ -69,7 +69,6 @@ public class LegacyBookmarksStoreMigration { } // swiftlint:disable cyclomatic_complexity - // swiftlint:disable function_body_length private static func migrate(source: NSManagedObjectContext, destination: NSManagedObjectContext) { @@ -189,6 +188,4 @@ public class LegacyBookmarksStoreMigration { } } // swiftlint:enable cyclomatic_complexity - // swiftlint:enable function_body_length - } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 1d553eea15..03d502f38c 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -26,7 +26,6 @@ import NetworkProtection extension Pixel { - // swiftlint:disable:next type_body_length public enum Event { case appLaunch @@ -1432,7 +1431,6 @@ extension Pixel.Event { } } -// swiftlint:disable file_length extension Pixel.Event { public enum BucketAggregation: String, CustomStringConvertible { diff --git a/Core/UserAgentManager.swift b/Core/UserAgentManager.swift index fecbbbf503..3ce83b099a 100644 --- a/Core/UserAgentManager.swift +++ b/Core/UserAgentManager.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// swiftlint:disable file_length - import BrowserServicesKit import Common import Foundation @@ -103,7 +101,6 @@ struct UserAgent { } private enum Constants { - // swiftlint:disable line_length static let fallbackWekKitVersion = "605.1.15" static let fallbackSafariComponent = "Safari/\(fallbackWekKitVersion)" static let fallbackDefaultAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_5 like Mac OS X) AppleWebKit/\(fallbackWekKitVersion) (KHTML, like Gecko) Mobile/15E148" @@ -122,7 +119,6 @@ struct UserAgent { static let uaVersionsKey = "versions" static let uaStateKey = "state" - // swiftlint:enable line_length } private struct Regex { @@ -193,7 +189,6 @@ struct UserAgent { return versions } - // swiftlint:disable:next cyclomatic_complexity public func agent(forUrl url: URL?, isDesktop: Bool, privacyConfig: PrivacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig) -> String { diff --git a/DuckDuckGo/AppDelegate+AppDeepLinks.swift b/DuckDuckGo/AppDelegate+AppDeepLinks.swift index 3aac5b325b..5cb1e9204f 100644 --- a/DuckDuckGo/AppDelegate+AppDeepLinks.swift +++ b/DuckDuckGo/AppDelegate+AppDeepLinks.swift @@ -22,7 +22,6 @@ import Core extension AppDelegate { - // swiftlint:disable:next cyclomatic_complexity func handleAppDeepLink(_ app: UIApplication, _ mainViewController: MainViewController?, _ url: URL) -> Bool { guard let mainViewController else { return false } diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 3cdd86b290..3b80abc268 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -41,10 +41,7 @@ import NetworkProtection import WebKit #endif -// swiftlint:disable file_length -// swiftlint:disable type_body_length @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - // swiftlint:enable type_body_length private static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) private struct ShortcutKey { @@ -104,7 +101,6 @@ import WebKit AppDependencyProvider.shared.accountManager } - // swiftlint:disable:next function_body_length cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // SKAD4 support @@ -474,7 +470,6 @@ import WebKit } } - // swiftlint:disable:next function_body_length func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 6cc6b5c117..228093422b 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -90,7 +90,6 @@ class AppDependencyProvider: DependencyProvider { let connectionObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession() let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) - // swiftlint:disable:next function_body_length init() { featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 388c4bd0a2..a9cefce342 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -22,8 +22,6 @@ import Bookmarks import Core import WidgetKit -// swiftlint:disable file_length -// swiftlint:disable:next type_body_length public class AppUserDefaults: AppSettings { public struct Notifications { diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index bc85b57395..e071b9c58a 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -24,8 +24,6 @@ import BrowserServicesKit import UserScript import PrivacyDashboard -// swiftlint:disable file_length - protocol AutoconsentPreferences { var autoconsentEnabled: Bool { get set } } diff --git a/DuckDuckGo/AutofillLoginDetailsView.swift b/DuckDuckGo/AutofillLoginDetailsView.swift index f8e940ee7f..d645e9b346 100644 --- a/DuckDuckGo/AutofillLoginDetailsView.swift +++ b/DuckDuckGo/AutofillLoginDetailsView.swift @@ -21,9 +21,6 @@ import SwiftUI import DuckUI import DesignResourcesKit -// swiftlint:disable file_length -// swiftlint:disable type_body_length - struct AutofillLoginDetailsView: View { @ObservedObject var viewModel: AutofillLoginDetailsViewModel @State private var actionSheetConfirmDeletePresented: Bool = false @@ -647,5 +644,3 @@ private struct Constants { static let textFieldTapSize: CGFloat = 36 static let insets = EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) } - -// swiftlint:enable type_body_length diff --git a/DuckDuckGo/AutofillLoginDetailsViewModel.swift b/DuckDuckGo/AutofillLoginDetailsViewModel.swift index 1f5f9dd659..4a7ee89493 100644 --- a/DuckDuckGo/AutofillLoginDetailsViewModel.swift +++ b/DuckDuckGo/AutofillLoginDetailsViewModel.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// swiftlint:disable file_length - import BrowserServicesKit import Common import Core @@ -41,8 +39,6 @@ struct ConfirmationAlert { var button: String } -// swiftlint:disable type_body_length - final class AutofillLoginDetailsViewModel: ObservableObject { enum ViewMode { case edit @@ -286,7 +282,6 @@ final class AutofillLoginDetailsViewModel: ObservableObject { } } - // swiftlint:disable:next cyclomatic_complexity func save() { guard let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) else { return @@ -455,8 +450,6 @@ final class AutofillLoginDetailsViewModel: ObservableObject { } } -// swiftlint:enable type_body_length - final class AutofillLoginDetailsHeaderViewModel: ObservableObject { private var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 50d82418e5..a479786f29 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -50,7 +50,6 @@ internal enum EnableAutofillRows: Int, CaseIterable { case resetNeverPromptWebsites } -// swiftlint:disable file_length type_body_length final class AutofillLoginListViewModel: ObservableObject { enum ViewState { @@ -409,7 +408,6 @@ final class AutofillLoginListViewModel: ObservableObject { } } } -// swiftlint:enable type_body_length extension AutofillLoginListItemViewModel: Comparable { static func < (lhs: AutofillLoginListItemViewModel, rhs: AutofillLoginListItemViewModel) -> Bool { diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index c30b49c694..500f9f3216 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -26,8 +26,6 @@ import DDGSync import DesignResourcesKit import SwiftUI -// swiftlint:disable file_length type_body_length - enum AutofillSettingsSource: String { case settings case overflow = "overflow_menu" @@ -1034,4 +1032,3 @@ extension AutofillLoginSettingsListViewController { ) } } -// swiftlint:enable file_length type_body_length diff --git a/DuckDuckGo/BookmarksViewController.swift b/DuckDuckGo/BookmarksViewController.swift index 7ec78e8881..b5034b411f 100644 --- a/DuckDuckGo/BookmarksViewController.swift +++ b/DuckDuckGo/BookmarksViewController.swift @@ -29,9 +29,6 @@ import Combine import Persistence import WidgetKit -// swiftlint:disable file_length -// swiftlint:disable type_body_length - class BookmarksViewController: UIViewController, UITableViewDelegate { private enum Constants { @@ -976,6 +973,3 @@ extension BookmarksViewController: AddOrEditBookmarkViewControllerDelegate { } } - -// swiftlint:enable type_body_length -// swiftlint:enable file_length diff --git a/DuckDuckGo/ConfigurationDebugViewController.swift b/DuckDuckGo/ConfigurationDebugViewController.swift index 1c8afce50a..05fc818d7b 100644 --- a/DuckDuckGo/ConfigurationDebugViewController.swift +++ b/DuckDuckGo/ConfigurationDebugViewController.swift @@ -113,7 +113,6 @@ class ConfigurationDebugViewController: UITableViewController { return titles[section] } - // swiftlint:disable cyclomatic_complexity override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) switch Sections(rawValue: indexPath.section) { @@ -159,7 +158,6 @@ class ConfigurationDebugViewController: UITableViewController { return cell } - // swiftlint:enable cyclomatic_complexity override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Sections(rawValue: section) { diff --git a/DuckDuckGo/CrashCollectionOnboardingView.swift b/DuckDuckGo/CrashCollectionOnboardingView.swift index 5790714770..65f7241eba 100644 --- a/DuckDuckGo/CrashCollectionOnboardingView.swift +++ b/DuckDuckGo/CrashCollectionOnboardingView.swift @@ -162,7 +162,6 @@ private struct ScrollDisabledIfAvailable: ViewModifier { #Preview { let model = CrashCollectionOnboardingViewModel(appSettings: AppDependencyProvider.shared.appSettings) - // swiftlint:disable:next line_length model.setReportDetails(with: ["test report details test report details test report details test report details test report details\n\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details\ntest report details".data(using: .utf8)!]) return CrashCollectionOnboardingView(model: model) } diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index 5e346d62c1..d89dc6b1a4 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -24,9 +24,6 @@ import BrowserServicesKit import Common import PrivacyDashboard -// swiftlint:disable file_length -// swiftlint:disable type_body_length - protocol EntityProviding { func entity(forHost host: String) -> Entity? @@ -404,5 +401,3 @@ final class DaxDialogs { return entity.domains?.contains(where: { MajorTrackers.domains.contains($0) }) ?? false ? entity : nil } } -// swiftlint:enable type_body_length -// swiftlint:enable file_length diff --git a/DuckDuckGo/EmailManagerRequestDelegate.swift b/DuckDuckGo/EmailManagerRequestDelegate.swift index 47f3b0f253..361ab26e99 100644 --- a/DuckDuckGo/EmailManagerRequestDelegate.swift +++ b/DuckDuckGo/EmailManagerRequestDelegate.swift @@ -32,7 +32,6 @@ extension EmailManagerRequestDelegate { } // swiftlint:enable unused_setter_value - // swiftlint:disable function_parameter_count func emailManager(_ emailManager: EmailManager, requested url: URL, method: String, headers: [String: String], parameters: [String: String]?, httpBody: Data?, timeoutInterval: TimeInterval) async throws -> Data { let finalURL = url.appendingParameters(parameters ?? [:]) var request = URLRequest(url: finalURL, timeoutInterval: timeoutInterval) @@ -51,7 +50,6 @@ extension EmailManagerRequestDelegate { return data } - // swiftlint:enable function_parameter_count func emailManagerKeychainAccessFailed(_ emailManager: EmailManager, accessType: EmailKeychainAccessType, diff --git a/DuckDuckGo/EmailSignupViewController.swift b/DuckDuckGo/EmailSignupViewController.swift index 640bb81526..e0c405ec61 100644 --- a/DuckDuckGo/EmailSignupViewController.swift +++ b/DuckDuckGo/EmailSignupViewController.swift @@ -27,7 +27,6 @@ import WebKit import DesignResourcesKit import SecureStorage -// swiftlint:disable file_length class EmailSignupViewController: UIViewController { private enum Constants { @@ -341,7 +340,6 @@ extension EmailSignupViewController: UserContentControllerDelegate { extension EmailSignupViewController: EmailManagerRequestDelegate { - // swiftlint:disable function_parameter_count func emailManager(_ emailManager: EmailManager, requested url: URL, method: String, headers: [String: String], parameters: [String: String]?, httpBody: Data?, timeoutInterval: TimeInterval) async throws -> Data { let method = APIRequest.HTTPMethod(rawValue: method) ?? .post let configuration = APIRequest.Configuration(url: url, @@ -353,8 +351,6 @@ extension EmailSignupViewController: EmailManagerRequestDelegate { let request = APIRequest(configuration: configuration, urlSession: .session()) return try await request.fetch().data ?? { throw AliasRequestError.noDataError }() } - // swiftlint:enable function_parameter_count - } // MARK: - SecureVaultManagerDelegate @@ -384,7 +380,6 @@ extension EmailSignupViewController: SecureVaultManagerDelegate { // no-op } - // swiftlint:disable function_parameter_count func secureVaultManager(_: SecureVaultManager, promptUserToAutofillCredentialsForDomain domain: String, withAccounts accounts: [SecureVaultModels.WebsiteAccount], @@ -393,7 +388,6 @@ extension EmailSignupViewController: SecureVaultManagerDelegate { completionHandler: @escaping (SecureVaultModels.WebsiteAccount?) -> Void) { // no-op } - // swiftlint:enable function_parameter_count func secureVaultManager(_: SecureVaultManager, promptUserWithGeneratedPassword password: String, @@ -465,5 +459,3 @@ extension EmailSignupViewController { } } - -// swiftlint:enable file_length diff --git a/DuckDuckGo/Favicons.swift b/DuckDuckGo/Favicons.swift index 8ecdc75bb0..e38acc128c 100644 --- a/DuckDuckGo/Favicons.swift +++ b/DuckDuckGo/Favicons.swift @@ -24,7 +24,6 @@ import UIKit import LinkPresentation import WidgetKit -// swiftlint:disable type_body_length file_length public class Favicons { public struct Constants { @@ -513,4 +512,3 @@ extension Favicons: Bookmarks.FaviconStoring { } } } -// swiftlint:enable type_body_length file_length diff --git a/DuckDuckGo/FeedbackUserText.swift b/DuckDuckGo/FeedbackUserText.swift index 6f74964472..df94a0baaa 100644 --- a/DuckDuckGo/FeedbackUserText.swift +++ b/DuckDuckGo/FeedbackUserText.swift @@ -19,7 +19,6 @@ import Foundation -// swiftlint:disable line_length extension UserText { public static let siteFeedbackTitle = NSLocalizedString("siteFeedback.title", @@ -227,4 +226,3 @@ extension UserText { comment: "") } -// swiftlint:enable line_length diff --git a/DuckDuckGo/FileSizeDebugViewController.swift b/DuckDuckGo/FileSizeDebugViewController.swift index 233ac2c0eb..07bbe1a7a3 100644 --- a/DuckDuckGo/FileSizeDebugViewController.swift +++ b/DuckDuckGo/FileSizeDebugViewController.swift @@ -127,7 +127,7 @@ class FileSizeDebugViewController: UITableViewController { return cell } - // swiftlint:disable:next cyclomatic_complexity function_body_length + // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = model[indexPath.row] diff --git a/DuckDuckGo/HomeScreenTransition.swift b/DuckDuckGo/HomeScreenTransition.swift index 72763a52c6..8cc3c9f664 100644 --- a/DuckDuckGo/HomeScreenTransition.swift +++ b/DuckDuckGo/HomeScreenTransition.swift @@ -91,8 +91,7 @@ class FromHomeScreenTransition: HomeScreenTransition { super.init(tabSwitcherViewController: tabSwitcherViewController) } - - // swiftlint:disable function_body_length + override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { prepareSubviews(using: transitionContext) @@ -166,12 +165,10 @@ class FromHomeScreenTransition: HomeScreenTransition { transitionContext.completeTransition(true) }) } - // swiftlint:enable function_body_length } class ToHomeScreenTransition: HomeScreenTransition { - // swiftlint:disable function_body_length override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { prepareSubviews(using: transitionContext) @@ -239,6 +236,4 @@ class ToHomeScreenTransition: HomeScreenTransition { transitionContext.completeTransition(true) }) } - // swiftlint:enable function_body_length - } diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index dad56cca12..12323c3891 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -77,7 +77,6 @@ class HomeViewController: UIViewController, NewTabPage { private var viewModelCancellable: AnyCancellable? private var favoritesDisplayModeCancellable: AnyCancellable? - // swiftlint:disable:next function_parameter_count static func loadFromStoryboard( homePageConfiguration: HomePageConfiguration, model: Tab, diff --git a/DuckDuckGo/MainViewController+Email.swift b/DuckDuckGo/MainViewController+Email.swift index 99f3942227..4209cf50dc 100644 --- a/DuckDuckGo/MainViewController+Email.swift +++ b/DuckDuckGo/MainViewController+Email.swift @@ -61,7 +61,6 @@ extension MainViewController { // MARK: - EmailManagerRequestDelegate extension MainViewController: EmailManagerRequestDelegate { - // swiftlint:disable function_parameter_count func emailManager(_ emailManager: EmailManager, requested url: URL, method: String, headers: HTTPHeaders, parameters: [String: String]?, httpBody: Data?, timeoutInterval: TimeInterval) async throws -> Data { let method = APIRequest.HTTPMethod(rawValue: method) ?? .post let configuration = APIRequest.Configuration(url: url, @@ -73,8 +72,6 @@ extension MainViewController: EmailManagerRequestDelegate { let request = APIRequest(configuration: configuration, urlSession: .session()) return try await request.fetch().data ?? { throw AliasRequestError.noDataError }() } - // swiftlint:enable function_parameter_count - } // MARK: - EmailManagerAliasPermissionDelegate diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index f125b722da..f943b83853 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -38,10 +38,7 @@ import SwiftUI import NetworkProtection #endif -// swiftlint:disable file_length -// swiftlint:disable type_body_length class MainViewController: UIViewController { - // swiftlint:enable type_body_length override var preferredStatusBarStyle: UIStatusBarStyle { return ThemeManager.shared.currentTheme.statusBarStyle @@ -2622,5 +2619,3 @@ extension MainViewController: AutofillLoginSettingsListViewControllerDelegate { controller.dismiss(animated: true) } } - -// swiftlint:enable file_length diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index c37fa4c53a..b625dea250 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// swiftlint:disable file_length - import UIKit #if !NETWORK_PROTECTION @@ -35,7 +33,6 @@ import NetworkExtension import NetworkProtection import Subscription -// swiftlint:disable:next type_body_length final class NetworkProtectionDebugViewController: UITableViewController { private let titles = [ Sections.featureVisibility: "Feature Visibility", @@ -177,7 +174,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { return titles[section] } - // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) @@ -229,7 +225,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { return cell } - // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Sections(rawValue: section) { case .clearData: return ClearDataRows.allCases.count @@ -247,7 +242,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { } } - // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Sections(rawValue: indexPath.section) { case .clearData: @@ -739,5 +733,3 @@ extension NWConnection { } #endif - -// swiftlint:enable file_length diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index 88ee269f64..2509a21e73 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// swiftlint:disable file_length - #if NETWORK_PROTECTION import Foundation @@ -68,7 +66,6 @@ struct NetworkProtectionLocationStatusModel { } } -// swiftlint:disable:next type_body_length final class NetworkProtectionStatusViewModel: ObservableObject { enum Constants { @@ -466,5 +463,3 @@ private extension ConnectionStatus { } #endif - -// swiftlint:enable file_length diff --git a/DuckDuckGo/OmniBar.swift b/DuckDuckGo/OmniBar.swift index 92afafa7b8..f020b9a6d8 100644 --- a/DuckDuckGo/OmniBar.swift +++ b/DuckDuckGo/OmniBar.swift @@ -29,8 +29,6 @@ public enum OmniBarIcon: String { case duckPlayer = "DuckPlayerURLIcon" } -// swiftlint:disable file_length -// swiftlint:disable type_body_length class OmniBar: UIView { public static let didLayoutNotification = Notification.Name("com.duckduckgo.app.OmniBarDidLayout") @@ -512,7 +510,6 @@ class OmniBar: UIView { } } -// swiftlint:enable type_body_length extension OmniBar: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { @@ -589,4 +586,3 @@ extension OmniBar { } } } -// swiftlint:enable file_length diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift index ebc1319fcf..7bcaa5299f 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift @@ -25,8 +25,6 @@ import BrowserServicesKit import PrivacyDashboard import Common -// swiftlint:disable file_length - extension PixelExperiment { static var privacyDashboardVariant: PrivacyDashboardVariant { diff --git a/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift index ef47b85c6c..01b4d32cf7 100644 --- a/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift +++ b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift @@ -45,7 +45,6 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let appSettings: AppSettings let internalUserDecider: InternalUserDecider - // swiftlint:disable:next function_body_length func refreshConfigMatcher(using store: RemoteMessagingStoring) async -> RemoteMessagingConfigMatcher { var bookmarksCount = 0 diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 74a4202ed2..f6df435588 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -16,7 +16,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// swiftlint:disable file_length + import Core import BrowserServicesKit import Persistence @@ -31,7 +31,6 @@ import Subscription import NetworkProtection #endif -// swiftlint:disable type_body_length final class SettingsViewModel: ObservableObject { // Dependencies @@ -351,7 +350,6 @@ final class SettingsViewModel: ObservableObject { appDataClearingObserver = nil } } -// swiftlint:enable type_body_length // MARK: Private methods extension SettingsViewModel { @@ -542,7 +540,6 @@ extension SettingsViewModel { // can review and migrate extension SettingsViewModel { - // swiftlint:disable:next cyclomatic_complexity @MainActor func presentLegacyView(_ view: SettingsLegacyViewProvider.LegacyView) { switch view { @@ -773,4 +770,3 @@ extension SettingsViewModel { } } -// swiftlint:enable file_length diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index d28469bc02..a4f4815214 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// swiftlint:disable file_length - import BrowserServicesKit import Common import Foundation @@ -33,7 +31,6 @@ enum SubscriptionTransactionStatus { } @available(iOS 15.0, *) -// swiftlint:disable:next type_body_length final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObject { struct Constants { @@ -210,7 +207,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } } - // swiftlint:disable:next function_body_length func subscriptionSelected(params: Any, original: WKScriptMessage) async -> Encodable? { DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) @@ -455,4 +451,3 @@ private extension Pixel { } } -// swiftlint:enable file_length diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index ee917228b5..95d0a9f5c3 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -24,7 +24,6 @@ import Core import Subscription @available(iOS 15.0, *) -// swiftlint:disable:next type_body_length final class SubscriptionFlowViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript @@ -143,8 +142,7 @@ final class SubscriptionFlowViewModel: ObservableObject { .store(in: &cancellables) } - - // swiftlint:disable cyclomatic_complexity + @MainActor private func handleTransactionError(error: SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError) { @@ -206,7 +204,6 @@ final class SubscriptionFlowViewModel: ObservableObject { DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseFailure) } } - // swiftlint:enable cyclomatic_complexity private func setupWebViewObservers() async { webViewModel.$navigationError diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 3436895730..9a2dba8630 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -26,7 +26,6 @@ import Core import NetworkProtection #endif -// swiftlint:disable:next type_body_length @available(iOS 15.0, *) final class SubscriptionDebugViewController: UITableViewController { let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) @@ -148,7 +147,6 @@ import NetworkProtection } } - // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Sections(rawValue: indexPath.section) { case .authorization: diff --git a/DuckDuckGo/SyncDebugViewController.swift b/DuckDuckGo/SyncDebugViewController.swift index 186e74d75d..4a1be4486f 100644 --- a/DuckDuckGo/SyncDebugViewController.swift +++ b/DuckDuckGo/SyncDebugViewController.swift @@ -99,7 +99,6 @@ class SyncDebugViewController: UITableViewController { return titles[section] } - // swiftlint:disable:next cyclomatic_complexity function_body_length override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) @@ -184,7 +183,6 @@ class SyncDebugViewController: UITableViewController { } } - // swiftlint:disable:next cyclomatic_complexity function_body_length override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Sections(rawValue: indexPath.section) { case .info: diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index efc05ddacf..b1dc787e10 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -24,7 +24,6 @@ import SyncUI import DDGSync import Common -// swiftlint:disable file_length @MainActor class SyncSettingsViewController: UIHostingController { diff --git a/DuckDuckGo/TabSwitcherViewController.swift b/DuckDuckGo/TabSwitcherViewController.swift index 9cac70c4d9..545698b9d0 100644 --- a/DuckDuckGo/TabSwitcherViewController.swift +++ b/DuckDuckGo/TabSwitcherViewController.swift @@ -25,7 +25,6 @@ import WebKit import Bookmarks import Persistence -// swiftlint:disable file_length class TabSwitcherViewController: UIViewController { struct Constants { @@ -543,4 +542,3 @@ extension TabSwitcherViewController { collectionView.reloadData() } } -// swiftlint:enable file_length diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 07bab32c00..55788c0840 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -41,18 +41,12 @@ import ContentScopeScripts import NetworkProtection #endif -// swiftlint:disable file_length -// swiftlint:disable type_body_length class TabViewController: UIViewController { -// swiftlint:enable type_body_length private struct Constants { static let frameLoadInterruptedErrorCode = 102 - static let trackerNetworksAnimationDelay: TimeInterval = 0.7 - static let secGPCHeader = "Sec-GPC" - static let navigationExpectationInterval = 3.0 } @@ -1469,8 +1463,7 @@ extension TabViewController: WKNavigationDelegate { return request } - - // swiftlint:disable function_body_length + // swiftlint:disable cyclomatic_complexity func webView(_ webView: WKWebView, @@ -1600,7 +1593,6 @@ extension TabViewController: WKNavigationDelegate { decisionHandler(decision) } } - // swiftlint:enable function_body_length // swiftlint:enable cyclomatic_complexity private func shouldWaitUntilContentBlockingIsLoaded(_ completion: @Sendable @escaping @MainActor () -> Void) -> Bool { @@ -1623,7 +1615,6 @@ extension TabViewController: WKNavigationDelegate { return true } - // swiftlint:disable function_body_length private func decidePolicyFor(navigationAction: WKNavigationAction, completion: @escaping (WKNavigationActionPolicy) -> Void) { let allowPolicy = determineAllowPolicy() @@ -1690,8 +1681,6 @@ extension TabViewController: WKNavigationDelegate { completion(.cancel) } } - // swiftlint:enable function_body_length - private func inferLoadContext(for navigationAction: WKNavigationAction) -> BrokenSiteReport.OpenerContext? { guard navigationAction.navigationType != .reload else { return nil } @@ -2581,7 +2570,6 @@ extension TabViewController: SecureVaultManagerDelegate { } } - // swiftlint:disable function_parameter_count func secureVaultManager(_: SecureVaultManager, promptUserToAutofillCredentialsForDomain domain: String, withAccounts accounts: [SecureVaultModels.WebsiteAccount], @@ -2615,7 +2603,6 @@ extension TabViewController: SecureVaultManagerDelegate { completionHandler(nil) } } - // swiftlint:enable function_parameter_count func secureVaultManager(_: SecureVaultManager, promptUserWithGeneratedPassword password: String, @@ -2639,7 +2626,6 @@ extension TabViewController: SecureVaultManagerDelegate { } /// Using Bool for detent size parameter to be backward compatible with iOS 14 - // swiftlint:disable function_parameter_count func presentAutofillPromptViewController(accountMatches: AccountMatches, domain: String, trigger: AutofillUserScript.GetTriggerType, @@ -2681,7 +2667,6 @@ extension TabViewController: SecureVaultManagerDelegate { } self.present(autofillPromptViewController, animated: true, completion: nil) } - // swiftlint:enable function_parameter_count // Used on macOS to request authentication for individual autofill items func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, @@ -2842,5 +2827,3 @@ extension UserContentController { } } - -// swiftlint:enable file_length diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index eb42751226..64c8618c33 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -26,7 +26,6 @@ import WidgetKit import Common import PrivacyDashboard -// swiftlint:disable file_length extension TabViewController { func buildBrowsingMenuHeaderContent() -> [BrowsingMenuEntry] { diff --git a/DuckDuckGo/VPNWaitlistUserText.swift b/DuckDuckGo/VPNWaitlistUserText.swift index 08c685910a..9d6b8e4f13 100644 --- a/DuckDuckGo/VPNWaitlistUserText.swift +++ b/DuckDuckGo/VPNWaitlistUserText.swift @@ -19,7 +19,6 @@ import Foundation -// swiftlint:disable line_length struct VPNWaitlistUserText { static let networkProtectionPrivacyPolicySection1Title = "We don’t ask for any personal information from you in order to use this beta service." @@ -64,4 +63,3 @@ struct VPNWaitlistUserText { static let networkProtectionTermsOfServiceSection8List = "You may be asked during the beta period to provide feedback about your experience. Doing so is optional and your feedback may be used to improve the service.\n\nIf you have enabled notifications for the DuckDuckGo app, we may use notifications to ask about your experience. You can disable notifications if you do not want to receive them." } -// swiftlint:enable line_length diff --git a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift index 006ee3dbc0..6dd76d3a18 100644 --- a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift +++ b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift @@ -26,7 +26,6 @@ import Combine @testable import BrowserServicesKit @testable import Common -// swiftlint:disable line_length file_length class AutofillLoginListViewModelTests: XCTestCase { private let tld = TLD() @@ -445,4 +444,3 @@ class AutofillLoginListItemViewModelTests: XCTestCase { XCTAssertEqual(result.count, 1) } } -// swiftlint:enable line_length diff --git a/DuckDuckGoTests/AutofillLoginPromptViewModelTests.swift b/DuckDuckGoTests/AutofillLoginPromptViewModelTests.swift index 90fa858318..b1a7cd2533 100644 --- a/DuckDuckGoTests/AutofillLoginPromptViewModelTests.swift +++ b/DuckDuckGoTests/AutofillLoginPromptViewModelTests.swift @@ -21,7 +21,6 @@ import XCTest @testable import DuckDuckGo @testable import BrowserServicesKit -// swiftlint:disable:next type_body_length final class AutofillLoginPromptViewModelTests: XCTestCase { func testWhenOnePerfectMatchAndNoPartialMatchesThenOnePerfectMatchShownAndMoreOptionsNotShown() { diff --git a/DuckDuckGoTests/BookmarksExporterTests.swift b/DuckDuckGoTests/BookmarksExporterTests.swift index 5d68848704..12d3fe02c2 100644 --- a/DuckDuckGoTests/BookmarksExporterTests.swift +++ b/DuckDuckGoTests/BookmarksExporterTests.swift @@ -197,7 +197,6 @@ class BookmarksExporterTests: XCTestCase { } } - // swiftlint:disable:next function_body_length func buildCommonContent(level: Int = 2) -> String { return [ BookmarksExporter.Template.openFolder(level: level, named: "FolderA-Level1"), diff --git a/DuckDuckGoTests/DailyPixelTests.swift b/DuckDuckGoTests/DailyPixelTests.swift index f8636d3e69..c03c635fb3 100644 --- a/DuckDuckGoTests/DailyPixelTests.swift +++ b/DuckDuckGoTests/DailyPixelTests.swift @@ -23,8 +23,6 @@ import OHHTTPStubsSwift import Networking @testable import Core -// swiftlint:disable type_body_length - final class DailyPixelTests: XCTestCase { let host = "improving.duckduckgo.com" @@ -363,5 +361,3 @@ final class DailyPixelTests: XCTestCase { case testError } } - -// swiftlint:enable type_body_length diff --git a/DuckDuckGoTests/DaxDialogTests.swift b/DuckDuckGoTests/DaxDialogTests.swift index d987fba05c..9d222aa239 100644 --- a/DuckDuckGoTests/DaxDialogTests.swift +++ b/DuckDuckGoTests/DaxDialogTests.swift @@ -97,7 +97,6 @@ final class DaxDialog: XCTestCase { func testWhenEachVersionOfTrackersMessageIsShownThenFormattedCorrectlyAndNotShownAgain() { - // swiftlint:disable line_length let testCases = [ (urls: [ URLs.google ], expected: DaxDialogs.BrowsingSpec.withOneTracker.format(args: "Google"), line: #line), (urls: [ URLs.google, URLs.amazon ], expected: DaxDialogs.BrowsingSpec.withMultipleTrackers.format(args: 0, "Google", "Amazon.com"), line: #line), @@ -106,7 +105,6 @@ final class DaxDialog: XCTestCase { (urls: [ URLs.facebook, URLs.google, URLs.amazon ], expected: DaxDialogs.BrowsingSpec.withMultipleTrackers.format(args: 1, "Google", "Facebook"), line: #line), (urls: [ URLs.facebook, URLs.google, URLs.amazon, URLs.tracker ], expected: DaxDialogs.BrowsingSpec.withMultipleTrackers.format(args: 2, "Google", "Facebook"), line: #line) ] - // swiftlint:enable line_length testCases.forEach { testCase in diff --git a/DuckDuckGoTests/LargeOmniBarStateTests.swift b/DuckDuckGoTests/LargeOmniBarStateTests.swift index 32e3b04bfc..0df7041e5b 100644 --- a/DuckDuckGoTests/LargeOmniBarStateTests.swift +++ b/DuckDuckGoTests/LargeOmniBarStateTests.swift @@ -22,9 +22,6 @@ import XCTest @testable import Core @testable import DuckDuckGo -// swiftlint:disable type_body_length -// swiftlint:disable file_length - class LargeOmniBarStateTests: XCTestCase { var mockDependencyProvider: MockDependencyProvider! @@ -464,5 +461,3 @@ class LargeOmniBarStateTests: XCTestCase { XCTAssertEqual(testee.onBrowsingStoppedState.name, LargeOmniBarState.HomeNonEditingState().name) } } -// swiftlint:enable type_body_length -// swiftlint:enable file_length diff --git a/DuckDuckGoTests/MockSecureVault.swift b/DuckDuckGoTests/MockSecureVault.swift index 2bdc097a81..5b089a6d44 100644 --- a/DuckDuckGoTests/MockSecureVault.swift +++ b/DuckDuckGoTests/MockSecureVault.swift @@ -22,7 +22,6 @@ import Foundation import GRDB import SecureStorage -// swiftlint:disable file_length typealias MockVaultFactory = SecureVaultFactory> // swiftlint:disable:next identifier_name diff --git a/DuckDuckGoTests/NetworkProtectionVPNLocationViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionVPNLocationViewModelTests.swift index 1691661ff8..f9c27f9cf0 100644 --- a/DuckDuckGoTests/NetworkProtectionVPNLocationViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionVPNLocationViewModelTests.swift @@ -23,9 +23,6 @@ import NetworkExtension import NetworkProtectionTestUtils @testable import DuckDuckGo -// swiftlint:disable type_body_length -// swiftlint:disable file_length - final class NetworkProtectionVPNLocationViewModelTests: XCTestCase { private var listRepository: MockNetworkProtectionLocationListRepository! private var settings: VPNSettings! @@ -657,8 +654,6 @@ final class NetworkProtectionVPNLocationViewModelTests: XCTestCase { } } -// swiftlint:enable type_body_length - final class MockNetworkProtectionLocationListRepository: NetworkProtectionLocationListRepository { var stubLocationList: [NetworkProtectionLocation] = [] var stubError: Error? @@ -690,5 +685,3 @@ struct EncodableWrapper: Encodable { try self.wrapped.encode(to: encoder) } } - -// swiftlint:enable file_length diff --git a/DuckDuckGoTests/SmallOmniBarStateTests.swift b/DuckDuckGoTests/SmallOmniBarStateTests.swift index 81308a1a13..d7b2f0d3e1 100644 --- a/DuckDuckGoTests/SmallOmniBarStateTests.swift +++ b/DuckDuckGoTests/SmallOmniBarStateTests.swift @@ -22,9 +22,6 @@ import Foundation import XCTest @testable import DuckDuckGo -// swiftlint:disable type_body_length -// swiftlint:disable file_length - class SmallOmniBarStateTests: XCTestCase { var mockDependencyProvider: MockDependencyProvider! @@ -466,6 +463,3 @@ class SmallOmniBarStateTests: XCTestCase { XCTAssertEqual(testee.onBrowsingStoppedState.name, SmallOmniBarState.HomeNonEditingState().name) } } - -// swiftlint:enable type_body_length -// swiftlint:enable file_length diff --git a/DuckDuckGoTests/UserAgentTests.swift b/DuckDuckGoTests/UserAgentTests.swift index e3745595aa..b8caff7f71 100644 --- a/DuckDuckGoTests/UserAgentTests.swift +++ b/DuckDuckGoTests/UserAgentTests.swift @@ -23,12 +23,10 @@ import XCTest @testable import Core -// swiftlint:disable file_length type_body_length final class UserAgentTests: XCTestCase { private struct DefaultAgent { - // swiftlint:disable line_length static let mobile = "Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" static let tablet = "Mozilla/5.0 (iPad; CPU OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" static let oldWebkitVersionMobile = "Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.14 (KHTML, like Gecko) Mobile/15E148" @@ -60,8 +58,6 @@ final class UserAgentTests: XCTestCase { static let mobileClosest = "Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.4 Mobile/15E148 Safari/604.1" static let tabletClosest = "Mozilla/5.0 (iPad; CPU OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.4 Mobile/15E148 Safari/604.1" static let desktopClosest = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" - // swiftlint:enable line_length - } private struct Constants { @@ -450,4 +446,3 @@ final class UserAgentTests: XCTestCase { } } -// swiftlint:enable file_length type_body_length diff --git a/FingerprintingUITests/FingerprintUITest.swift b/FingerprintingUITests/FingerprintUITest.swift index 2982f4ce45..bd5c5a869c 100644 --- a/FingerprintingUITests/FingerprintUITest.swift +++ b/FingerprintingUITests/FingerprintUITest.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// swiftlint:disable line_length - import XCTest class FingerprintUITest: XCTestCase { @@ -161,7 +159,7 @@ class FingerprintUITest: XCTestCase { } extension XCUIElement { - + // https://stackoverflow.com/a/38523252 public func clear() { guard let stringValue = self.value as? String else { @@ -175,7 +173,4 @@ extension XCUIElement { let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) self.typeText(deleteString) } - } - -// swiftlint:enable line_length diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift index 1ab3d0bf18..b6358b37ea 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift @@ -19,7 +19,6 @@ import Foundation -// swiftlint:disable line_length public struct UserText { // Sync Title @@ -170,6 +169,4 @@ public struct UserText { static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync.unavailable", bundle: Bundle.module, value: "Sync & Backup is Unavailable", comment: "Title of the warning message") static let syncUnavailableMessage = NSLocalizedString("sync.warning.data.syncing.disabled", bundle: Bundle.module, value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data.syncing.disabled.upgrade.required", bundle: Bundle.module, value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") - - // swiftlint:enable line_length } diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 4f2a0eaa5c..3c5cc27184 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -29,8 +29,6 @@ import NetworkProtection import Subscription import WidgetKit -// swiftlint:disable type_body_length - // Initial implementation for initial Network Protection tests. Will be fleshed out with https://app.asana.com/0/1203137811378537/1204630829332227/f final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { @@ -207,7 +205,6 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { // MARK: - Error Reporting - // swiftlint:disable:next cyclomatic_complexity function_body_length private static func networkProtectionDebugEvents(controllerErrorStore: NetworkProtectionTunnelErrorStore) -> EventMapping? { return EventMapping { event, _, _, _ in let pixelEvent: Pixel.Event @@ -445,7 +442,4 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { } } } - -// swiftlint:enable type_body_length -// swiftlint:disable:next file_length #endif diff --git a/PacketTunnelProvider/UserText.swift b/PacketTunnelProvider/UserText.swift index 7243db5e2b..c95fe5c868 100644 --- a/PacketTunnelProvider/UserText.swift +++ b/PacketTunnelProvider/UserText.swift @@ -19,7 +19,6 @@ import Foundation -// swiftlint:disable line_length final class UserText { // MARK: - Network Protection Notifications @@ -43,4 +42,3 @@ final class UserText { static let networkProtectionEntitlementExpiredNotificationBody = NSLocalizedString("network.protection.entitlement.expired.notification.body", value: "VPN disconnected due to expired subscription. Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "The body of the notification when Privacy Pro subscription expired") } -// swiftlint:enable line_length diff --git a/Widgets/WidgetViews.swift b/Widgets/WidgetViews.swift index 71a2c99bf6..2215b1aead 100644 --- a/Widgets/WidgetViews.swift +++ b/Widgets/WidgetViews.swift @@ -21,7 +21,6 @@ import SwiftUI import WidgetKit import DesignResourcesKit -// swiftlint:disable file_length struct FavoriteView: View { var favorite: Favorite? @@ -410,4 +409,3 @@ struct WidgetViews_Previews: PreviewProvider { .environment(\.colorScheme, .dark) } } -// swiftlint:enable file_length From be68a3ec5c1d6a71e9ea8b369ed345d88b3a0f69 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 10 Jul 2024 14:10:50 +0100 Subject: [PATCH 07/48] Update BSK version (#3064) Task/Issue URL: https://app.asana.com/0/72649045549333/1207776486350131/f Description: Update BSK version, no changes for iOS --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 01a7899b30..fcc5249d06 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9946,7 +9946,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 169.0.0; + version = 169.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 804d062cbb..afb5649d29 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "bfabf4518a33eb2b4b11003a15633a24c28fa922", - "version" : "169.0.0" + "revision" : "522a9defa0eb951c9101d9f54c19d51f8ef57f4c", + "version" : "169.1.0" } }, { From 27d649b3ac0886a788b9553d0b9dbb3b81f7f38a Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 10 Jul 2024 18:08:07 +0200 Subject: [PATCH 08/48] [DuckPlayer] 5. First batch of pixels (#3061) ask/Issue URL: https://app.asana.com/0/1204099484721401/1207768980322967/f Description: Implements the first batch of pixels for DuckPlayer, incluiding: m_duck-player_daily-unique-view m_duck-player_view-from_youtube_main-overla m_duck-player_view-from_youtube_automatic m_duck-player_view-from_serp m_duck-player_view-from_other m_duck-player_setting_always_settings m_duck-player_overlay_youtube_impressions m_duck-player_overlay_youtube_watch_here m_duck-player_setting_always_duck-player m_duck-player_setting_back-to-default m_watch-in-duckplayer_initial_u --- Core/PixelEvent.swift | 41 ++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 6 +- .../DuckPlayer/DuckNavigationHandling.swift | 1 + DuckDuckGo/DuckPlayer/DuckPlayer.swift | 5 +- .../DuckPlayer/DuckPlayerURLExtension.swift | 6 ++ .../YouTubePlayerNavigationHandler.swift | 63 +++++++++++++++++-- .../DuckPlayer/YoutubeOverlayUserScript.swift | 37 +++++++++-- DuckDuckGo/SettingsViewModel.swift | 11 ++++ DuckDuckGo/TabViewController.swift | 6 ++ ...YoutublePlayerNavigationHandlerTests.swift | 4 +- 10 files changed, 162 insertions(+), 18 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 03d502f38c..ba00d96d2a 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -720,6 +720,27 @@ extension Pixel { case favoriteLaunchedNTPDaily case bookmarkLaunchedDaily case newTabPageDisplayedDaily + + // MARK: DuckPlayer + case duckPlayerDailyUniqueView + case duckPlayerViewFromYoutubeViaMainOverlay + case duckPlayerViewFromYoutubeViaHoverButton + case duckPlayerViewFromYoutubeAutomatic + case duckPlayerViewFromSERP + case duckPlayerViewFromOther + case duckPlayerOverlayYoutubeImpressions + case duckPlayerOverlayYoutubeWatchHere + case duckPlayerSettingAlwaysDuckPlayer + case duckPlayerSettingAlwaysOverlaySERP + case duckPlayerSettingAlwaysOverlayYoutube + case duckPlayerSettingAlwaysSettings + case duckPlayerSettingNeverOverlaySERP + case duckPlayerSettingNeverOverlayYoutube + case duckPlayerSettingNeverSettings + case duckPlayerSettingBackToDefault + case duckPlayerWatchOnYoutube + case watchInDuckPlayerInitial + } } @@ -1427,6 +1448,26 @@ extension Pixel.Event { case .favoriteLaunchedNTPDaily: return "m_favorite_launched_ntp_daily" case .bookmarkLaunchedDaily: return "m_bookmark_launched_daily" case .newTabPageDisplayedDaily: return "m_new_tab_page_displayed_daily" + + // MARK: DuckPlayer + case .duckPlayerDailyUniqueView: return "m_duck-player_daily-unique-view" + case .duckPlayerViewFromYoutubeViaMainOverlay: return "m_duck-player_view-from_youtube_main-overlay" + case .duckPlayerViewFromYoutubeViaHoverButton: return "m_duck-player_view-from_youtube_hover-button" + case .duckPlayerViewFromYoutubeAutomatic: return "m_duck-player_view-from_youtube_automatic" + case .duckPlayerViewFromSERP: return "m_duck-player_view-from_serp" + case .duckPlayerViewFromOther: return "m_duck-player_view-from_other" + case .duckPlayerSettingAlwaysSettings: return "m_duck-player_setting_always_settings" + case .duckPlayerOverlayYoutubeImpressions: return "m_duck-player_overlay_youtube_impressions" + case .duckPlayerOverlayYoutubeWatchHere: return "m_duck-player_overlay_youtube_watch_here" + case .duckPlayerSettingAlwaysDuckPlayer: return "m_duck-player_setting_always_duck-player" + case .duckPlayerSettingAlwaysOverlaySERP: return "m_duck-player_setting_always_overlay_serp" + case .duckPlayerSettingAlwaysOverlayYoutube: return "m_duck-player_setting_always_overlay_youtube" + case .duckPlayerSettingNeverOverlaySERP: return "m_duck-player_setting_never_overlay_serp" + case .duckPlayerSettingNeverOverlayYoutube: return "m_duck-player_setting_never_overlay_youtube" + case .duckPlayerSettingNeverSettings: return "m_duck-player_setting_never_settings" + case .duckPlayerSettingBackToDefault: return "m_duck-player_setting_back-to-default" + case .duckPlayerWatchOnYoutube: return "m_duck-player_watch_on_youtube" + case .watchInDuckPlayerInitial: return "m_watch-in-duckplayer_initial_u" } } } diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index afb5649d29..6a4c6b33bc 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,10 +138,10 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" } }, { diff --git a/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift b/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift index 809889a797..c2e2178472 100644 --- a/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift +++ b/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift @@ -20,6 +20,7 @@ import WebKit protocol DuckNavigationHandling { + var referrer: DuckPlayerReferrer { get set } func handleNavigation(_ navigationAction: WKNavigationAction, webView: WKWebView, completion: @escaping (WKNavigationActionPolicy) -> Void) diff --git a/DuckDuckGo/DuckPlayer/DuckPlayer.swift b/DuckDuckGo/DuckPlayer/DuckPlayer.swift index c883848a5b..fb54fd00b9 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayer.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayer.swift @@ -54,6 +54,10 @@ public struct UserValues: Codable { let askModeOverlayHidden: Bool } +public enum DuckPlayerReferrer { + case youtube, other +} + protocol DuckPlayerProtocol { var settings: DuckPlayerSettingsProtocol { get } @@ -66,7 +70,6 @@ protocol DuckPlayerProtocol { func initialSetup(params: Any, message: WKScriptMessage) async -> Encodable? } - final class DuckPlayer: DuckPlayerProtocol { static let duckPlayerHost: String = "player" diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift b/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift index 96f1b0eaeb..d17a9b3780 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift @@ -112,6 +112,12 @@ extension URL { } + var isYoutube: Bool { + guard let host else { return false } + return host == "m.youtube.com" || host == "youtube.com" + + } + private func addingTimestamp(_ timestamp: String?) -> URL { guard let timestamp = timestamp, let regex = try? NSRegularExpression(pattern: "^(\\d+[smh]?)+$"), diff --git a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift index e38bb6a728..d28415eec7 100644 --- a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift @@ -20,20 +20,38 @@ import Foundation import ContentScopeScripts import WebKit +import Core final class YoutubePlayerNavigationHandler { var duckPlayer: DuckPlayerProtocol + var referrer: DuckPlayerReferrer = .other { + didSet { + print(referrer) + } + } + + private struct Constants { + static let SERPURL = "https://duckduckgo.com/" + static let refererHeader = "Referer" + static let templateDirectory = "pages/duckplayer" + static let templateName = "index" + static let templateExtension = "html" + static let localhost = "http://localhost" + static let duckPlayerAlwaysString = "always" + static let duckPlayerDefaultString = "default" + static let settingsKey = "settings" + static let httpMethod = "GET" + } init(duckPlayer: DuckPlayerProtocol) { self.duckPlayer = duckPlayer } - private static let templateDirectory = "pages/duckplayer" - private static let templateName = "index" - static var htmlTemplatePath: String { - guard let file = ContentScopeScripts.Bundle.path(forResource: Self.templateName, ofType: "html", inDirectory: Self.templateDirectory) else { + guard let file = ContentScopeScripts.Bundle.path(forResource: Constants.templateName, + ofType: Constants.templateExtension, + inDirectory: Constants.templateDirectory) else { assertionFailure("YouTube Private Player HTML template not found") return "" } @@ -50,8 +68,8 @@ final class YoutubePlayerNavigationHandler { static func makeDuckPlayerRequest(for videoID: String, timestamp: String?) -> URLRequest { var request = URLRequest(url: .youtubeNoCookie(videoID, timestamp: timestamp)) - request.addValue("http://localhost/", forHTTPHeaderField: "Referer") - request.httpMethod = "GET" + request.addValue(Constants.localhost, forHTTPHeaderField: Constants.refererHeader) + request.httpMethod = Constants.httpMethod return request } @@ -87,6 +105,21 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { webView: WKWebView, completion: @escaping (WKNavigationActionPolicy) -> Void) { + // Daily Unique View Pixel + if let url = navigationAction.request.url, + url.isDuckPlayer, + duckPlayer.settings.mode != .disabled { + let setting = duckPlayer.settings.mode == .enabled ? Constants.duckPlayerAlwaysString : Constants.duckPlayerDefaultString + DailyPixel.fire(pixel: Pixel.Event.duckPlayerDailyUniqueView, withAdditionalParameters: [Constants.settingsKey: setting]) + } + + // Pixel for Views From Youtube + if let url = navigationAction.request.url, + referrer == .youtube, + duckPlayer.settings.mode == .enabled { + Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromYoutubeAutomatic, debounce: 2) + } + // If DuckPlayer is Enabled or in ask mode, render the video if let url = navigationAction.request.url, url.isDuckURLScheme, @@ -117,6 +150,7 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { // such as changes triggered via JS @MainActor func handleURLChange(url: URL?, webView: WKWebView) { + if let url = url, url.isYoutubeVideo, !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, @@ -125,6 +159,7 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { let newURL = URL.duckPlayer(videoID, timestamp: timestamp) webView.load(URLRequest(url: newURL)) } + } // DecidePolicyFor handler to redirect relevant requests @@ -133,6 +168,22 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, completion: @escaping (WKNavigationActionPolicy) -> Void, webView: WKWebView) { + + // Pixel for Views From SERP + if let url = navigationAction.request.url, + navigationAction.request.allHTTPHeaderFields?[Constants.refererHeader] == Constants.SERPURL, + duckPlayer.settings.mode == .enabled, !url.isDuckPlayer { + Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromSERP, debounce: 2) + } + + // Pixel for views from Other Sites + if let url = navigationAction.request.url, + navigationAction.request.allHTTPHeaderFields?[Constants.refererHeader] != Constants.SERPURL, + duckPlayer.settings.mode == .enabled, !url.isDuckPlayer { + Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromOther, debounce: 2) + } + + if let url = navigationAction.request.url, url.isYoutubeVideo, !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, diff --git a/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift index f7228f11a7..091afb731b 100644 --- a/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift @@ -22,18 +22,22 @@ import WebKit import Common import UserScript import Combine +import Core +import BrowserServicesKit final class YoutubeOverlayUserScript: NSObject, Subfeature { var duckPlayer: DuckPlayerProtocol private var cancellables = Set() + var statisticsStore: StatisticsStore struct Constants { static let featureName = "duckPlayer" } - init(duckPlayer: DuckPlayerProtocol) { + init(duckPlayer: DuckPlayerProtocol, statisticsStore: StatisticsStore = StatisticsUserDefaults()) { self.duckPlayer = duckPlayer + self.statisticsStore = statisticsStore super.init() subscribeToDuckPlayerMode() } @@ -71,6 +75,7 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { static let getUserValues = "getUserValues" static let openDuckPlayer = "openDuckPlayer" static let sendDuckPlayerPixel = "sendDuckPlayerPixel" + static let initialSetup = "initialSetup" } weak var broker: UserScriptMessageBroker? @@ -101,6 +106,8 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { return openDuckPlayer case Handlers.sendDuckPlayerPixel: return handleSendJSPixel + case Handlers.initialSetup: + return duckPlayer.initialSetup default: assertionFailure("YoutubeOverlayUserScript: Failed to parse User Script message: \(methodName)") // TODO: Send pixel here @@ -143,11 +150,29 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { extension YoutubeOverlayUserScript { @MainActor func handleSendJSPixel(params: Any, message: UserScriptMessage) -> Encodable? { - // guard let body = message.messageBody as? [String: Any], let parameters = body["params"] as? [String: Any] else { - // return nil - // } - // let pixelName = parameters["pixelName"] as? String - // To be implemented at a later point + guard let body = message.messageBody as? [String: Any], let parameters = body["params"] as? [String: Any] else { + return nil + } + let pixelName = parameters["pixelName"] as? String + + switch pixelName { + case "play.use": + Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromYoutubeViaMainOverlay) + + if let installDate = statisticsStore.installDate, + installDate > Date.yearAgo { + UniquePixel.fire(pixel: Pixel.Event.watchInDuckPlayerInitial) + } + + case "play.do_not_use": + Pixel.fire(pixel: Pixel.Event.duckPlayerOverlayYoutubeWatchHere) + + case "overlay": + Pixel.fire(pixel: Pixel.Event.duckPlayerOverlayYoutubeImpressions) + + default: + break + } return nil } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index f6df435588..d07dd01080 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -259,6 +259,17 @@ final class SettingsViewModel: ObservableObject { set: { self.appSettings.duckPlayerMode = $0 self.state.duckPlayerMode = $0 + + switch self.state.duckPlayerMode { + case .alwaysAsk: + Pixel.fire(pixel: Pixel.Event.duckPlayerSettingBackToDefault) + case .disabled: + Pixel.fire(pixel: Pixel.Event.duckPlayerSettingNeverSettings) + case .enabled: + Pixel.fire(pixel: Pixel.Event.duckPlayerSettingAlwaysSettings) + default: + break + } } ) } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 55788c0840..2b994c0b6e 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -660,6 +660,12 @@ class TabViewController: UIViewController { handler.handleURLChange(url: url, webView: webView) } } + + if var handler = youtubeNavigationHandler, + let url { + handler.referrer = url.isYoutube ? .youtube : .other + + } } func enableFireproofingForDomain(_ domain: String) { diff --git a/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift b/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift index 32fc0565ae..fe4f76eb09 100644 --- a/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift +++ b/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift @@ -67,7 +67,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { XCTAssertEqual(duckPlayerRequest.url?.host, "www.youtube-nocookie.com") XCTAssertEqual(duckPlayerRequest.url?.path, "/embed/abc123") XCTAssertEqual(duckPlayerRequest.url?.query?.contains("t=10s"), true) - XCTAssertEqual(duckPlayerRequest.value(forHTTPHeaderField: "Referer"), "http://localhost/") + XCTAssertEqual(duckPlayerRequest.value(forHTTPHeaderField: "Referer"), "http://localhost") XCTAssertEqual(duckPlayerRequest.httpMethod, "GET") } @@ -81,7 +81,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { XCTAssertEqual(duckPlayerRequest.url?.host, "www.youtube-nocookie.com") XCTAssertEqual(duckPlayerRequest.url?.path, "/embed/abc123") XCTAssertEqual(duckPlayerRequest.url?.query?.contains("t=10s"), true) - XCTAssertEqual(duckPlayerRequest.value(forHTTPHeaderField: "Referer"), "http://localhost/") + XCTAssertEqual(duckPlayerRequest.value(forHTTPHeaderField: "Referer"), "http://localhost") XCTAssertEqual(duckPlayerRequest.httpMethod, "GET") } From 9b5d74729f7c23123e53930b5e18132aefc019ea Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 10 Jul 2024 18:10:51 +0200 Subject: [PATCH 09/48] Fix search bar selection when coming back to the app (#3065) Task/Issue URL: https://app.asana.com/0/414709148257752/1207736781372157/f Tech Design URL: CC: Description: Fix Selection when on the serp anc coming back to the app. Steps to test this PR: Press fire button. Ensure the URL bar is selected and keyboard is up. Leave the app for 6s. Come back to the app - search for something. Tap on search bar - text should not be selected, cursor should be at the end. Hide keyboard, then background the app for 6s. Come back to the app -> tap on search bar. Whole text should be selected. Dismiss keyboard, tap on search again. Text should not be selected. Navigate to non-serp page. Tap on the url - it should be selected. --- DuckDuckGo/MainViewController.swift | 2 ++ DuckDuckGo/en.lproj/Localizable.strings | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index f943b83853..05ae10a1cd 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -873,6 +873,8 @@ class MainViewController: UIViewController { currentTab?.url?.absoluteString ?? "") return } + // Make sure that once query is submitted, we don't trigger the non-SERP flow + skipSERPFlow = false loadUrl(url) } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 354b38f728..de9933f9a0 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -826,7 +826,7 @@ /* Alert action for starting a file dowload */ "downloads.alert.action.save-to-downloads" = "Save to Downloads"; -/* Cancel download action for alert when trying to cancel the file download */ +/* Cancel download action for downloads */ "downloads.cancel-download.alert.cancel" = "Cancel"; /* Message for alert when trying to cancel the file download */ From f508e41baffd69ed4d6e7a19a66800aa14be94ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:04:34 -0400 Subject: [PATCH 10/48] Bump submodules/privacy-reference-tests from `a603ff9` to `a242bf0` (#3067) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- submodules/privacy-reference-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index a603ff9af2..a242bf03ff 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8 +Subproject commit a242bf03ff33b573eb716405b15924cc712d41c1 From 22220731a86b8e4d69c13fad2227d4f2e4984fa6 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 11 Jul 2024 15:37:48 +0100 Subject: [PATCH 11/48] ui tests for content blocking and toggling protections (#3060) --- .maestro/release_tests/content-blocking.yaml | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .maestro/release_tests/content-blocking.yaml diff --git a/.maestro/release_tests/content-blocking.yaml b/.maestro/release_tests/content-blocking.yaml new file mode 100644 index 0000000000..51c0a7aaf4 --- /dev/null +++ b/.maestro/release_tests/content-blocking.yaml @@ -0,0 +1,54 @@ +# content-blocking.yaml +appId: com.duckduckgo.mobile.ios +tags: + - release + +--- + +# Set up +- clearState +- launchApp +- runFlow: + file: ../shared/onboarding.yaml + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/" +- pressKey: Enter + +# Manage onboarding +- runFlow: + file: ../shared/onboarding_browsing.yaml + +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/tracker-reporting/1major-via-script.html" +- pressKey: Enter + +- assertVisible: "1 major tracker loaded via script src" +- tapOn: + id: PrivacyIcon +- assertVisible: "Protections are ON for this site" + +- assertVisible: "We blocked Google Ads (Google) from loading tracking requests on this page." + +- tapOn: "View Tracker Companies" +- assertVisible: "doubleclick.net" +- assertVisible: "Back" +- tapOn: "Back" + +- assertVisible: "Disable Protections" +- tapOn: "Disable Protections" +- assertVisible: "Site not working? Let us know." +- assertVisible: "Don't Send" +- tapOn: "Don't Send" + +- tapOn: "Refresh Page" +- assertVisible: "1 major tracker loaded via script src" + +- tapOn: + id: PrivacyIcon +- assertVisible: "Protections are OFF for this site" From ee79a54f5804028a0b66f7ea3a9e13bd72a9ca7b Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Thu, 11 Jul 2024 13:34:05 -0500 Subject: [PATCH 12/48] Add locale to broken site report (#3069) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index fcc5249d06..3a4f43b6ac 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9946,7 +9946,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 169.1.0; + version = 169.1.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6a4c6b33bc..13817beaa1 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "522a9defa0eb951c9101d9f54c19d51f8ef57f4c", - "version" : "169.1.0" + "revision" : "284748afd9c9f1b1962391857f67ff4735e1313f", + "version" : "169.1.1" } }, { From 5c9fc24c251f1e68ee57e54d86d20bbdbcb3cb0c Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 12 Jul 2024 13:14:50 +0600 Subject: [PATCH 13/48] Upload exception messages to Sentry (#2974) Task/Issue URL: https://app.asana.com/0/1202406491309510/1207597188072658/f Tech Design URL: https://app.asana.com/0/1202406491309510/1207311510824180/f BSK PR: duckduckgo/BrowserServicesKit#856 macOS PR: duckduckgo/macos-browser#2886 --- Core/PixelEvent.swift | 4 ++- Core/UserDefaultsPropertyWrapper.swift | 1 + DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 10 +++---- DuckDuckGo/AppDelegate.swift | 29 +++++++++++++++---- DuckDuckGo/Debug.storyboard | 9 ++++++ DuckDuckGo/RootDebugViewController.swift | 16 ++++++---- 7 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index ba00d96d2a..a3287ee7f8 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -435,7 +435,8 @@ extension Pixel { // MARK: debug pixels case dbCrashDetected - + case crashOnCrashHandlersSetUp + case dbMigrationError case dbRemovalError case dbDestroyError @@ -1152,6 +1153,7 @@ extension Pixel.Event { // MARK: debug pixels case .dbCrashDetected: return "m_d_crash" + case .crashOnCrashHandlersSetUp: return "m_d_crash_on_handlers_setup" case .dbMigrationError: return "m_d_dbme" case .dbRemovalError: return "m_d_dbre" case .dbDestroyError: return "m_d_dbde" diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 3248521dd1..dd3d93e456 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -32,6 +32,7 @@ public struct UserDefaultsWrapper { case favorites = "com.duckduckgo.ios.home.favorites" case keyboardOnNewTab = "com.duckduckgo.ios.keyboard.newtab" case keyboardOnAppLaunch = "com.duckduckgo.ios.keyboard.applaunch" + case didCrashDuringCrashHandlersSetUp = "com.duckduckgo.ios.didCrashDuringCrashHandlersSetUp" case gridViewEnabled = "com.duckduckgo.ios.tabs.grid" case gridViewSeen = "com.duckduckgo.ios.tabs.seen" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3a4f43b6ac..77639462d9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9946,7 +9946,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 169.1.1; + version = 170.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 13817beaa1..a2b8213dba 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "284748afd9c9f1b1962391857f67ff4735e1313f", - "version" : "169.1.1" + "revision" : "33ceded0295158678da10d8ed685e64d3ad1a0d6", + "version" : "170.0.0" } }, { @@ -138,10 +138,10 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "46989693916f56d1186bd59ac15124caef896560", - "version" : "1.3.1" + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 3b80abc268..913e277426 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -101,6 +101,20 @@ import WebKit AppDependencyProvider.shared.accountManager } + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + private var didCrashDuringCrashHandlersSetUp: Bool + + override init() { + super.init() + + if !didCrashDuringCrashHandlersSetUp { + didCrashDuringCrashHandlersSetUp = true + CrashLogMessageExtractor.setUp() + didCrashDuringCrashHandlersSetUp = false + } + } + + // swiftlint:disable:next function_body_length cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // SKAD4 support @@ -139,19 +153,17 @@ import WebKit Configuration.setURLProvider(AppConfigurationURLProvider()) } - crashCollection.start { pixelParameters, payloads, sendReport in + crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in pixelParameters.forEach { params in Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) } // Async dispatch because rootViewController may otherwise be nil here DispatchQueue.main.async { - guard let viewController = self.window?.rootViewController else { - return - } - let dataPayloads = payloads.map { $0.jsonRepresentation() } + guard let viewController = self.window?.rootViewController else { return } + let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) - crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: dataPayloads, from: viewController, sendReport: sendReport) + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) self.crashReportUploaderOnboarding = crashReportUploaderOnboarding } } @@ -356,6 +368,11 @@ import WebKit setUpAutofillPixelReporter() + if didCrashDuringCrashHandlersSetUp { + Pixel.fire(pixel: .crashOnCrashHandlersSetUp) + didCrashDuringCrashHandlersSetUp = false + } + return true } diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index d7c8485d06..b0188cabd4 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -303,6 +303,15 @@ + + + + + + + + + diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index 305df76064..ffa87d2894 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -17,18 +17,19 @@ // limitations under the License. // -import UIKit -import LinkPresentation -import Core -import Kingfisher -import WebKit import BrowserServicesKit import Common import Configuration -import Persistence +import Core +import Crashes import DDGSync +import Kingfisher +import LinkPresentation import NetworkProtection +import Persistence import SwiftUI +import UIKit +import WebKit class RootDebugViewController: UITableViewController { @@ -37,6 +38,7 @@ class RootDebugViewController: UITableViewController { case crashFatalError = 666 case crashMemory = 667 case crashException = 673 + case crashCxxException = 675 case toggleInspectableWebViews = 668 case toggleInternalUserState = 669 case openVanillaBrowser = 670 @@ -144,6 +146,8 @@ class RootDebugViewController: UITableViewController { tableView.beginUpdates() tableView.deleteRows(at: [indexPath], with: .automatic) tableView.endUpdates() + case .crashCxxException: + throwTestCppExteption() case .toggleInspectableWebViews: let defaults = AppUserDefaults() defaults.inspectableWebViewEnabled.toggle() From 7dd241176f596de776ed5cd846e329436babb77c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 12 Jul 2024 11:09:27 +0100 Subject: [PATCH 14/48] ensure only one history database context (#3068) --- Core/HistoryManager.swift | 118 ++++++++++++------ DuckDuckGo/AppDelegate.swift | 60 ++++----- DuckDuckGo/AutocompleteViewController.swift | 4 +- DuckDuckGo/MainViewController.swift | 4 +- DuckDuckGo/SettingsViewModel.swift | 15 ++- DuckDuckGo/SuggestionTrayViewController.swift | 4 +- DuckDuckGo/TabManager.swift | 4 +- DuckDuckGo/TabViewController.swift | 6 +- DuckDuckGoTests/HistoryCaptureTests.swift | 20 ++- DuckDuckGoTests/HistoryManagerTests.swift | 48 +++++-- 10 files changed, 180 insertions(+), 103 deletions(-) diff --git a/Core/HistoryManager.swift b/Core/HistoryManager.swift index ab2e7785da..a6ef0a799d 100644 --- a/Core/HistoryManager.swift +++ b/Core/HistoryManager.swift @@ -25,9 +25,11 @@ import Common import Persistence public protocol HistoryManaging { - + var historyCoordinator: HistoryCoordinating { get } - func loadStore(onCleanFinished: @escaping () -> Void) throws + func isHistoryFeatureEnabled() -> Bool + var isEnabledByUser: Bool { get } + func removeAllHistory() async } @@ -44,43 +46,38 @@ public class HistoryManager: HistoryManaging { let privacyConfigManager: PrivacyConfigurationManaging let variantManager: VariantManager - let database: CoreDataDatabase let internalUserDecider: InternalUserDecider - let isEnabledByUser: () -> Bool - - private var currentHistoryCoordinator: HistoryCoordinating? + let dbCoordinator: HistoryCoordinator public var historyCoordinator: HistoryCoordinating { guard isHistoryFeatureEnabled(), - isEnabledByUser() else { - currentHistoryCoordinator = nil + isEnabledByUser else { return NullHistoryCoordinator() } + return dbCoordinator + } - if let currentHistoryCoordinator { - return currentHistoryCoordinator - } + public let isAutocompleteEnabledByUser: () -> Bool + public let isRecentlyVisitedSitesEnabledByUser: () -> Bool - let coordinator = makeDatabaseHistoryCoordinator() - coordinator.loadHistory { - // no-op - only done here in case it was flipped in settings - } - - currentHistoryCoordinator = coordinator - return coordinator + public var isEnabledByUser: Bool { + return isAutocompleteEnabledByUser() && isRecentlyVisitedSitesEnabledByUser() } - public init(privacyConfigManager: PrivacyConfigurationManaging, - variantManager: VariantManager, - database: CoreDataDatabase, - internalUserDecider: InternalUserDecider, - isEnabledByUser: @autoclosure @escaping () -> Bool) { + /// Use `make()` + init(privacyConfigManager: PrivacyConfigurationManaging, + variantManager: VariantManager, + internalUserDecider: InternalUserDecider, + dbCoordinator: HistoryCoordinator, + isAutocompleteEnabledByUser: @autoclosure @escaping () -> Bool, + isRecentlyVisitedSitesEnabledByUser: @autoclosure @escaping () -> Bool) { self.privacyConfigManager = privacyConfigManager self.variantManager = variantManager - self.database = database self.internalUserDecider = internalUserDecider - self.isEnabledByUser = isEnabledByUser + self.dbCoordinator = dbCoordinator + self.isAutocompleteEnabledByUser = isAutocompleteEnabledByUser + self.isRecentlyVisitedSitesEnabledByUser = isRecentlyVisitedSitesEnabledByUser } /// Determines if the history feature is enabled. This code will need to be cleaned up once the roll out is at 100% @@ -106,25 +103,12 @@ public class HistoryManager: HistoryManaging { public func removeAllHistory() async { await withCheckedContinuation { continuation in - historyCoordinator.burnAll { + dbCoordinator.burnAll { continuation.resume() } } } - public func loadStore(onCleanFinished: @escaping () -> Void) { - let coordinator = makeDatabaseHistoryCoordinator() - coordinator.loadHistory { - onCleanFinished() - } - } - - private func makeDatabaseHistoryCoordinator() -> HistoryCoordinator { - let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) - let historyCoordinator = HistoryCoordinator(historyStoring: HistoryStore(context: context, eventMapper: HistoryStoreEventMapper())) - return historyCoordinator - } - } class NullHistoryCoordinator: HistoryCoordinating { @@ -244,3 +228,59 @@ class HistoryStoreEventMapper: EventMapping { fatalError("Use init()") } } + +extension HistoryManager { + + /// Should only be called once in the app + public static func make(isAutocompleteEnabledByUser: @autoclosure @escaping () -> Bool, + isRecentlyVisitedSitesEnabledByUser: @autoclosure @escaping () -> Bool, + internalUserDecider: InternalUserDecider, + privacyConfigManager: PrivacyConfigurationManaging) -> Result { + + let database = HistoryDatabase.make() + var loadError: Error? + database.loadStore { _, error in + loadError = error + } + + if let loadError { + return .failure(loadError) + } + + let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) + let dbCoordinator = HistoryCoordinator(historyStoring: HistoryStore(context: context, eventMapper: HistoryStoreEventMapper())) + + let historyManager = HistoryManager(privacyConfigManager: privacyConfigManager, + variantManager: DefaultVariantManager(), + internalUserDecider: internalUserDecider, + dbCoordinator: dbCoordinator, + isAutocompleteEnabledByUser: isAutocompleteEnabledByUser(), + isRecentlyVisitedSitesEnabledByUser: isRecentlyVisitedSitesEnabledByUser()) + + dbCoordinator.loadHistory(onCleanFinished: { + // Do future migrations after clean has finished. See macOS for an example. + }) + + return .success(historyManager) + } + +} + +// Available in case `make` fails so that we don't have to pass optional around. +public struct NullHistoryManager: HistoryManaging { + + public var isEnabledByUser = false + + public let historyCoordinator: HistoryCoordinating = NullHistoryCoordinator() + + public func removeAllHistory() async { + // No-op + } + + public func isHistoryFeatureEnabled() -> Bool { + return false + } + + public init() { } + +} diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 92d0396ef9..59a72ccc66 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -279,9 +279,7 @@ import WebKit } let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager(AppDependencyProvider.shared.appSettings, - AppDependencyProvider.shared.internalUserDecider, - ContentBlocking.shared.privacyConfigurationManager) + let historyManager = makeHistoryManager() let tabsModel = prepareTabsModel(previewsSource: previewsSource) let main = MainViewController(bookmarksDatabase: bookmarksDatabase, @@ -346,6 +344,29 @@ import WebKit return true } + private func makeHistoryManager() -> HistoryManaging { + + let settings = AppDependencyProvider.shared.appSettings + + switch HistoryManager.make(isAutocompleteEnabledByUser: settings.autocomplete, + isRecentlyVisitedSitesEnabledByUser: settings.recentlyVisitedSites, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) { + + case .failure(let error): + Pixel.fire(pixel: .historyStoreLoadFailed, error: error) + if error.isDiskFull { + self.presentInsufficientDiskSpaceAlert() + } else { + self.presentPreemptiveCrashAlert() + } + return NullHistoryManager() + + case .success(let historyManager): + return historyManager + } + } + private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), appSettings: AppSettings = AppDependencyProvider.shared.appSettings, isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { @@ -367,39 +388,6 @@ import WebKit return tabsModel } - private func makeHistoryManager(_ appSettings: AppSettings, - _ internalUserDecider: InternalUserDecider, - _ privacyConfigManager: PrivacyConfigurationManaging) -> HistoryManager { - - let db = HistoryDatabase.make() - var loadError: Error? - db.loadStore { _, error in - loadError = error - } - - if let loadError { - Pixel.fire(pixel: .historyStoreLoadFailed, error: loadError) - if loadError.isDiskFull { - self.presentInsufficientDiskSpaceAlert() - } else { - self.presentPreemptiveCrashAlert() - } - } - - let historyManager = HistoryManager(privacyConfigManager: privacyConfigManager, - variantManager: DefaultVariantManager(), - database: db, - internalUserDecider: internalUserDecider, - isEnabledByUser: appSettings.recentlyVisitedSites) - - // Ensure we don't do this if the history is disabled in privacy confg - guard historyManager.isHistoryFeatureEnabled() else { return historyManager } - historyManager.loadStore(onCleanFinished: { - // Do future migrations after clean has finished. See macOS for an example. - }) - return historyManager - } - private func presentPreemptiveCrashAlert() { Task { @MainActor in let alertController = CriticalAlerts.makePreemptiveCrashAlert() diff --git a/DuckDuckGo/AutocompleteViewController.swift b/DuckDuckGo/AutocompleteViewController.swift index a2b417562f..d787885d5d 100644 --- a/DuckDuckGo/AutocompleteViewController.swift +++ b/DuckDuckGo/AutocompleteViewController.swift @@ -40,7 +40,7 @@ class AutocompleteViewController: UIHostingController { weak var delegate: AutocompleteViewControllerDelegate? weak var presentationDelegate: AutocompleteViewControllerPresentationDelegate? - private let historyManager: HistoryManager + private let historyManager: HistoryManaging var historyCoordinator: HistoryCoordinating { historyManager.historyCoordinator } @@ -63,7 +63,7 @@ class AutocompleteViewController: UIHostingController { private var historyMessageManager: HistoryMessageManager - init(historyManager: HistoryManager, + init(historyManager: HistoryManaging, bookmarksDatabase: CoreDataDatabase, appSettings: AppSettings, historyMessageManager: HistoryMessageManager = HistoryMessageManager()) { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 422e2eac4c..f75e8b497e 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -170,13 +170,13 @@ class MainViewController: UIViewController { fatalError("Use init?(code:") } - var historyManager: HistoryManager + var historyManager: HistoryManaging var viewCoordinator: MainViewCoordinator! init( bookmarksDatabase: CoreDataDatabase, bookmarksDatabaseCleaner: BookmarkDatabaseCleaner, - historyManager: HistoryManager, + historyManager: HistoryManaging, syncService: DDGSyncing, syncDataProviders: SyncDataProviders, appSettings: AppSettings, diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 74a4202ed2..abc2f7d96a 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -44,7 +44,7 @@ final class SettingsViewModel: ObservableObject { private let voiceSearchHelper: VoiceSearchHelperProtocol private let syncPausedStateManager: any SyncPausedStateManaging var emailManager: EmailManager { EmailManager() } - private let historyManager: HistoryManager + private let historyManager: HistoryManaging // Subscription Dependencies private let subscriptionManager: SubscriptionManager @@ -164,6 +164,7 @@ final class SettingsViewModel: ObservableObject { set: { self.appSettings.autocomplete = $0 self.state.autocomplete = $0 + self.clearHistoryIfNeeded() self.updateRecentlyVisitedSitesVisibility() if $0 { @@ -181,6 +182,7 @@ final class SettingsViewModel: ObservableObject { set: { self.appSettings.autocomplete = $0 self.state.autocomplete = $0 + self.clearHistoryIfNeeded() self.updateRecentlyVisitedSitesVisibility() if $0 { @@ -203,6 +205,7 @@ final class SettingsViewModel: ObservableObject { } else { Pixel.fire(pixel: .settingsRecentlyVisitedOff) } + self.clearHistoryIfNeeded() } ) } @@ -331,7 +334,7 @@ final class SettingsViewModel: ObservableObject { voiceSearchHelper: VoiceSearchHelperProtocol = AppDependencyProvider.shared.voiceSearchHelper, variantManager: VariantManager = AppDependencyProvider.shared.variantManager, deepLink: SettingsDeepLinkSection? = nil, - historyManager: HistoryManager, + historyManager: HistoryManaging, syncPausedStateManager: any SyncPausedStateManaging) { self.state = SettingsState.defaults @@ -401,6 +404,14 @@ extension SettingsViewModel { } } + private func clearHistoryIfNeeded() { + if !historyManager.isEnabledByUser { + Task { + await self.historyManager.removeAllHistory() + } + } + } + private func getNetworkProtectionState() -> SettingsState.NetworkProtection { return SettingsState.NetworkProtection(enabled: false, status: "") } diff --git a/DuckDuckGo/SuggestionTrayViewController.swift b/DuckDuckGo/SuggestionTrayViewController.swift index 0122b3219b..be3bc7a0c5 100644 --- a/DuckDuckGo/SuggestionTrayViewController.swift +++ b/DuckDuckGo/SuggestionTrayViewController.swift @@ -49,7 +49,7 @@ class SuggestionTrayViewController: UIViewController { private var willRemoveAutocomplete = false private let bookmarksDatabase: CoreDataDatabase private let favoritesModel: FavoritesListInteracting - private let historyManager: HistoryManager + private let historyManager: HistoryManaging var selectedSuggestion: Suggestion? { autocompleteController?.selectedSuggestion @@ -79,7 +79,7 @@ class SuggestionTrayViewController: UIViewController { } } - required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManager) { + required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging) { self.favoritesModel = favoritesViewModel self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index 7fd9b05d53..d0a73362c6 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -32,7 +32,7 @@ class TabManager { private var tabControllerCache = [TabViewController]() private let bookmarksDatabase: CoreDataDatabase - private let historyManager: HistoryManager + private let historyManager: HistoryManaging private let syncService: DDGSyncing private var previewsSource: TabPreviewsSource @@ -45,7 +45,7 @@ class TabManager { init(model: TabsModel, previewsSource: TabPreviewsSource, bookmarksDatabase: CoreDataDatabase, - historyManager: HistoryManager, + historyManager: HistoryManaging, syncService: DDGSyncing) { self.model = model self.previewsSource = previewsSource diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 12a6fdd025..c9c0577c41 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -298,7 +298,7 @@ class TabViewController: UIViewController { static func loadFromStoryboard(model: Tab, appSettings: AppSettings = AppDependencyProvider.shared.appSettings, bookmarksDatabase: CoreDataDatabase, - historyManager: HistoryManager, + historyManager: HistoryManaging, syncService: DDGSyncing) -> TabViewController { let storyboard = UIStoryboard(name: "Tab", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "TabViewController", creator: { coder in @@ -316,7 +316,7 @@ class TabViewController: UIViewController { (webView.configuration.userContentController as? UserContentController)! } - let historyManager: HistoryManager + let historyManager: HistoryManaging let historyCapture: HistoryCapture var duckPlayer: DuckPlayerProtocol = DuckPlayer() @@ -326,7 +326,7 @@ class TabViewController: UIViewController { tabModel: Tab, appSettings: AppSettings, bookmarksDatabase: CoreDataDatabase, - historyManager: HistoryManager, + historyManager: HistoryManaging, syncService: DDGSyncing) { self.tabModel = tabModel self.appSettings = appSettings diff --git a/DuckDuckGoTests/HistoryCaptureTests.swift b/DuckDuckGoTests/HistoryCaptureTests.swift index ff6ac01b92..dc4f5a9cfd 100644 --- a/DuckDuckGoTests/HistoryCaptureTests.swift +++ b/DuckDuckGoTests/HistoryCaptureTests.swift @@ -79,7 +79,10 @@ final class HistoryCaptureTests: XCTestCase { } func makeCapture() -> HistoryCapture { - return HistoryCapture(historyManager: MockHistoryManager(historyCoordinator: mockHistoryCoordinator)) + let mock = MockHistoryManager(historyCoordinator: mockHistoryCoordinator, + isEnabledByUser: true, + historyFeatureEnabled: true) + return HistoryCapture(historyManager: mock) } } @@ -107,11 +110,20 @@ private extension URL { class MockHistoryManager: HistoryManaging { let historyCoordinator: HistoryCoordinating + var isEnabledByUser: Bool + var historyFeatureEnabled: Bool - init(historyCoordinator: HistoryCoordinating) { + init(historyCoordinator: HistoryCoordinating, isEnabledByUser: Bool, historyFeatureEnabled: Bool) { self.historyCoordinator = historyCoordinator + self.historyFeatureEnabled = historyFeatureEnabled + self.isEnabledByUser = isEnabledByUser } - func loadStore(onCleanFinished: @escaping () -> Void) throws { + func isHistoryFeatureEnabled() -> Bool { + return historyFeatureEnabled } -} + + func removeAllHistory() async { + } + + } diff --git a/DuckDuckGoTests/HistoryManagerTests.swift b/DuckDuckGoTests/HistoryManagerTests.swift index 22ec55a5f3..74414ae853 100644 --- a/DuckDuckGoTests/HistoryManagerTests.swift +++ b/DuckDuckGoTests/HistoryManagerTests.swift @@ -30,7 +30,6 @@ final class HistoryManagerTests: XCTestCase { let privacyConfigManager = MockPrivacyConfigurationManager() var variantManager = MockVariantManager() let internalUserStore = MockInternalUserStoring() - var enabledByUser = true func test() { @@ -85,7 +84,8 @@ final class HistoryManagerTests: XCTestCase { XCTFail("DB Error \($0)") } - XCTAssertEqual(condition.expected, historyManager.isHistoryFeatureEnabled(), "\(index): \(condition)") + let result = historyManager.isHistoryFeatureEnabled() + XCTAssertEqual(condition.expected, result, "\(index): \(condition)") if condition.expected { XCTAssertTrue(historyManager.historyCoordinator is HistoryCoordinator) @@ -97,7 +97,7 @@ final class HistoryManagerTests: XCTestCase { } - func test_WhenUserHasDisabledSetting_ThenDontStoreOrLoadHistory() { + func test_WhenUserHasDisabledAutocompleteSitesSetting_ThenDontStoreOrLoadHistory() { privacyConfig.isFeatureKeyEnabled = { feature, _ in XCTAssertEqual(feature, .history) @@ -106,7 +106,29 @@ final class HistoryManagerTests: XCTestCase { internalUserStore.isInternalUser = true privacyConfigManager.privacyConfig = privacyConfig - enabledByUser = false + autocompleteEnabledByUser = false + + let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! + let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) + db.loadStore() + + let historyManager = makeHistoryManager(db) { + XCTFail("DB Error \($0)") + } + + XCTAssertTrue(historyManager.historyCoordinator is NullHistoryCoordinator) + } + + func test_WhenUserHasDisabledRecentlyVisitedSitesSetting_ThenDontStoreOrLoadHistory() { + + privacyConfig.isFeatureKeyEnabled = { feature, _ in + XCTAssertEqual(feature, .history) + return true + } + + internalUserStore.isInternalUser = true + privacyConfigManager.privacyConfig = privacyConfig + recentlyVisitedSitesEnabledByUser = false let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) @@ -120,16 +142,20 @@ final class HistoryManagerTests: XCTestCase { } private func makeHistoryManager(_ db: CoreDataDatabase, onStoreLoadFailed: @escaping (Error) -> Void) -> HistoryManager { - let manager = HistoryManager(privacyConfigManager: privacyConfigManager, + let eventMapper = HistoryStoreEventMapper() + let store = HistoryStore(context: db.makeContext(concurrencyType: .privateQueueConcurrencyType), eventMapper: eventMapper) + let dbCoordinator = HistoryCoordinator(historyStoring: store) + + return HistoryManager(privacyConfigManager: privacyConfigManager, variantManager: variantManager, - database: db, internalUserDecider: DefaultInternalUserDecider(mockedStore: internalUserStore), - isEnabledByUser: self.isEnabledByUser()) - return manager - } + dbCoordinator: dbCoordinator, + isAutocompleteEnabledByUser: self.autocompleteEnabledByUser, + isRecentlyVisitedSitesEnabledByUser: self.recentlyVisitedSitesEnabledByUser) - private func isEnabledByUser() -> Bool { - return enabledByUser } + var autocompleteEnabledByUser = true + var recentlyVisitedSitesEnabledByUser = true + } From 5fe2db41ef3a8f6f45561f240e44fb1c975a68c5 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 12 Jul 2024 11:26:10 +0100 Subject: [PATCH 15/48] Release 7.128.0-1 (#3073) --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++----------- fastlane/metadata/default/release_notes.txt | 1 - 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 57fc9a1480..d99b9972f5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8169,7 +8169,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8206,7 +8206,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8296,7 +8296,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8323,7 +8323,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8472,7 +8472,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8497,7 +8497,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8566,7 +8566,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8600,7 +8600,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8633,7 +8633,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8663,7 +8663,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8973,7 +8973,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9004,7 +9004,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9032,7 +9032,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9065,7 +9065,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9095,7 +9095,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9128,11 +9128,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9365,7 +9365,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9392,7 +9392,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9424,7 +9424,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9461,7 +9461,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9496,7 +9496,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9531,11 +9531,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9708,11 +9708,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9741,10 +9741,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 5d700d36bd..aab259d4e4 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,3 +1,2 @@ - Fixed an issue where cancelling downloads would sometimes not work as expected. - Make sure you have iOS 15 or later to keep getting the latest browser updates and improvements. Here's how to update iOS: https://support.apple.com/guide/iphone/update-ios-iph3e504502/14.0/ios/14.0 -Join our fully distributed team and help raise the standard of trust online! https://duckduckgo.com/hiring \ No newline at end of file From c09558cbf37a764150dc9ea3c1093500f14febab Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 12 Jul 2024 12:39:13 +0100 Subject: [PATCH 16/48] add autoclear tests (#3056) Co-authored-by: Bartek Waresiak --- .maestro/release_tests/autoclear.yaml | 102 ++++++++++++++++++++++ .maestro/release_tests/backgrounding.yaml | 1 + .maestro/run_ui_tests.sh | 16 +--- .maestro/setup_ui_tests.sh | 23 ++++- DuckDuckGo/AutoClear.swift | 4 + DuckDuckGo/Base.lproj/Settings.storyboard | 5 +- 6 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 .maestro/release_tests/autoclear.yaml diff --git a/.maestro/release_tests/autoclear.yaml b/.maestro/release_tests/autoclear.yaml new file mode 100644 index 0000000000..aca8dfed81 --- /dev/null +++ b/.maestro/release_tests/autoclear.yaml @@ -0,0 +1,102 @@ +# autoclear.yaml +appId: com.duckduckgo.mobile.ios +tags: + - release + +--- + +# Set up +- clearState +- launchApp: + appId: "com.duckduckgo.mobile.ios" + arguments: + "autoclear-ui-test": true + +- runFlow: + file: ../shared/onboarding.yaml + +# Enable autoclear + +- tapOn: "Settings" +- assertVisible: "Default Browser" +- scrollUntilVisible: + centerElement: true + element: + text: "Data Clearing" +- assertVisible: "Data Clearing" +- tapOn: "Data Clearing" +- assertVisible: "Off" +- assertVisible: "Automatically Clear Data" +- tapOn: "Automatically Clear Data" +- assertVisible: + id: "AutoclearEnabledToggle" +- tapOn: + id: "AutoclearEnabledToggle" +- assertVisible: "App Exit, Inactive for 5 Minutes" +- tapOn: "App Exit, Inactive for 5 Minutes" +- tapOn: "Data Clearing" # Uses the name of the previous page +- tapOn: "Settings" +- tapOn: "Done" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter + +# Manage onboarding +- runFlow: + file: ../shared/onboarding_browsing.yaml + +# Add a cookie +- assertVisible: "Storage Counter: undefined" +- assertVisible: "Cookie Counter:" +- assertNotVisible: "Cookie Counter: 1" +- assertNotVisible: "Storage Counter: 1" +- assertVisible: "Manual Increment" +- tapOn: "Manual Increment" +- assertVisible: "Cookie Counter: 1" +- assertVisible: "Storage Counter: 1" + +# Load a new tab +- longPressOn: "Tab Switcher" +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://example.com" +- pressKey: Enter + +# Go home and hover there a bit +- pressKey: Home + +- repeat: + times: 3 + commands: + - swipe: + start: 50%, 50% + end: 10%, 50% + - swipe: + start: 10%, 50% + end: 50%, 50% + +- tapOn: "DuckDuckGo" + +- assertNotVisible: "https://example.com/" +- assertVisible: "Search or enter address" +- tapOn: "Tab Switcher" +- assertNotVisible: "Example Domain" +- assertVisible: "1 Private Tab" +- tapOn: "Done" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter +- assertVisible: "Storage Counter: undefined" +- assertVisible: "Cookie Counter:" diff --git a/.maestro/release_tests/backgrounding.yaml b/.maestro/release_tests/backgrounding.yaml index addcea9a78..808880b347 100644 --- a/.maestro/release_tests/backgrounding.yaml +++ b/.maestro/release_tests/backgrounding.yaml @@ -8,6 +8,7 @@ tags: # Set up - clearState - launchApp + - runFlow: file: ../shared/onboarding.yaml diff --git a/.maestro/run_ui_tests.sh b/.maestro/run_ui_tests.sh index 0feebc7ca1..1556b9d83c 100755 --- a/.maestro/run_ui_tests.sh +++ b/.maestro/run_ui_tests.sh @@ -26,9 +26,6 @@ run_flow() { local flow=$2 echo "ℹ️ Deleting app in simulator $device_uuid" - - # Ignore result of this for now. The only error hopefully is that there was nothing to terminate - xcrun simctl terminate $device_uuid $app_bundle 2> /dev/null xcrun simctl uninstall $device_uuid $app_bundle if [ $? -ne 0 ]; then @@ -40,6 +37,7 @@ run_flow() { echo "⏲️ Starting flow $( basename $flow)" + export MAESTRO_DRIVER_STARTUP_TIMEOUT=60000 maestro --udid=$device_uuid test $flow if [ $? -ne 0 ]; then log_message $run_log "❌ FAIL: $flow" @@ -78,16 +76,8 @@ echo "ℹ️ Running UI tests for $1" device_uuid=$(cat $device_uuid_path) echo "ℹ️ using device $device_uuid" -killall Simulator - -xcrun simctl shutdown $device_uuid -xcrun simctl boot $device_uuid -if [ $? -ne 0 ]; then - echo "‼️ Unable to boot simulator" - exit 1 -fi - -open -a Simulator +# Simulator should already be up and running from running the setup script +# re-run the setup script with `--skip-build` to set up again echo "ℹ️ creating run log in $run_log" if [ -f $run_log ]; then diff --git a/.maestro/setup_ui_tests.sh b/.maestro/setup_ui_tests.sh index 64292c30a8..ae8fdead11 100755 --- a/.maestro/setup_ui_tests.sh +++ b/.maestro/setup_ui_tests.sh @@ -40,10 +40,11 @@ check_maestro() { } build_app() { - - if [ -d "$derived_data_path" ]; then + if [ -d "$derived_data_path" ] && [ "$1" -eq "0" ]; then echo "⚠️ Removing previously created $derived_data_path" rm -rf $derived_data_path + else + echo "ℹ️ Not cleaning derived data at $derived_data_path" fi echo "⏲️ Building the app" @@ -78,6 +79,8 @@ while [[ "$#" -gt 0 ]]; do case $1 in --skip-build) skip_build=1 ;; + --rebuild) + rebuild=1 ;; *) esac shift @@ -86,7 +89,7 @@ done if [ -n "$skip_build" ]; then echo "Skipping build" else - build_app + build_app $rebuild fi echo "ℹ️ Closing all simulators" @@ -109,6 +112,20 @@ if [ $? -ne 0 ]; then exit 1 fi +echo "ℹ️ Setting device locale to en_US" + +xcrun simctl spawn $device_uuid defaults write "Apple Global Domain" AppleLanguages -array en +if [ $? -ne 0 ]; then + echo "‼️ Unable to set preferred language" + exit 1 +fi + +xcrun simctl spawn $device_uuid defaults write "Apple Global Domain" AppleLocale -string en_US +if [ $? -ne 0 ]; then + echo "‼️ Unable to set region" + exit 1 +fi + open -a Simulator xcrun simctl install booted $app_location diff --git a/DuckDuckGo/AutoClear.swift b/DuckDuckGo/AutoClear.swift index a90b5f6278..2e1abbc02b 100644 --- a/DuckDuckGo/AutoClear.swift +++ b/DuckDuckGo/AutoClear.swift @@ -73,6 +73,10 @@ class AutoClear { private func shouldClearData(elapsedTime: TimeInterval) -> Bool { guard let settings = AutoClearSettingsModel(settings: appSettings) else { return false } + if ProcessInfo.processInfo.arguments.contains("autoclear-ui-test") { + return elapsedTime > 5 + } + switch settings.timing { case .termination: return false diff --git a/DuckDuckGo/Base.lproj/Settings.storyboard b/DuckDuckGo/Base.lproj/Settings.storyboard index 7f128195f3..b0decd8ad5 100644 --- a/DuckDuckGo/Base.lproj/Settings.storyboard +++ b/DuckDuckGo/Base.lproj/Settings.storyboard @@ -1,9 +1,9 @@ - + - + @@ -205,6 +205,7 @@ + From 3610c70be16ee7f98f38ae9ae6a45e3ad0c4f5d1 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 12 Jul 2024 14:50:52 +0100 Subject: [PATCH 17/48] remove unused gating logic for history roll out (#3075) --- Core/DefaultVariantManager.swift | 5 -- Core/FeatureFlag.swift | 3 - Core/HistoryManager.swift | 36 +--------- DuckDuckGo/AppDelegate.swift | 7 +- DuckDuckGoTests/HistoryManagerTests.swift | 88 +++++++---------------- 5 files changed, 30 insertions(+), 109 deletions(-) diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index 1be1051020..c8018ff0c2 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -26,7 +26,6 @@ extension FeatureName { // Define your feature e.g.: // public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature") - public static let history = FeatureName(rawValue: "history") } public struct VariantIOS: Variant { @@ -62,10 +61,6 @@ public struct VariantIOS: Variant { VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []), - // This needs to stay until we finish rolling out history to all users... - // This ensures that users who previously had do not lose it. - VariantIOS(name: "md", weight: doNotAllocate, isIncluded: When.inEnglish, features: [.history]), - returningUser ] diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 30d6911a51..bb05e0df29 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -32,7 +32,6 @@ public enum FeatureFlag: String { case incontextSignup case autoconsentOnByDefault case history - case historyRollout case newTabPageSections case duckPlayer } @@ -62,8 +61,6 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(AutoconsentSubfeature.onByDefault)) case .history: return .remoteReleasable(.feature(.history)) - case .historyRollout: - return .remoteReleasable(.subfeature(HistorySubFeature.onByDefault)) case .newTabPageSections: return .internalOnly case .duckPlayer: diff --git a/Core/HistoryManager.swift b/Core/HistoryManager.swift index a6ef0a799d..692d76efea 100644 --- a/Core/HistoryManager.swift +++ b/Core/HistoryManager.swift @@ -33,20 +33,9 @@ public protocol HistoryManaging { } -// Used for controlling incremental rollout -public enum HistorySubFeature: String, PrivacySubfeature { - public var parent: PrivacyFeature { - .history - } - - case onByDefault -} - public class HistoryManager: HistoryManaging { let privacyConfigManager: PrivacyConfigurationManaging - let variantManager: VariantManager - let internalUserDecider: InternalUserDecider let dbCoordinator: HistoryCoordinator public var historyCoordinator: HistoryCoordinating { @@ -66,15 +55,11 @@ public class HistoryManager: HistoryManaging { /// Use `make()` init(privacyConfigManager: PrivacyConfigurationManaging, - variantManager: VariantManager, - internalUserDecider: InternalUserDecider, dbCoordinator: HistoryCoordinator, isAutocompleteEnabledByUser: @autoclosure @escaping () -> Bool, isRecentlyVisitedSitesEnabledByUser: @autoclosure @escaping () -> Bool) { self.privacyConfigManager = privacyConfigManager - self.variantManager = variantManager - self.internalUserDecider = internalUserDecider self.dbCoordinator = dbCoordinator self.isAutocompleteEnabledByUser = isAutocompleteEnabledByUser self.isRecentlyVisitedSitesEnabledByUser = isRecentlyVisitedSitesEnabledByUser @@ -82,23 +67,7 @@ public class HistoryManager: HistoryManaging { /// Determines if the history feature is enabled. This code will need to be cleaned up once the roll out is at 100% public func isHistoryFeatureEnabled() -> Bool { - guard privacyConfigManager.privacyConfig.isEnabled(featureKey: .history) else { - // Whatever happens if this is disabled then disable the feature - return false - } - - if internalUserDecider.isInternalUser { - // Internal users get the feature - return true - } - - if variantManager.isSupported(feature: .history) { - // Users in the experiment get the feature - return true - } - - // Handles incremental roll out to everyone else - return privacyConfigManager.privacyConfig.isSubfeatureEnabled(HistorySubFeature.onByDefault) + return privacyConfigManager.privacyConfig.isEnabled(featureKey: .history) } public func removeAllHistory() async { @@ -234,7 +203,6 @@ extension HistoryManager { /// Should only be called once in the app public static func make(isAutocompleteEnabledByUser: @autoclosure @escaping () -> Bool, isRecentlyVisitedSitesEnabledByUser: @autoclosure @escaping () -> Bool, - internalUserDecider: InternalUserDecider, privacyConfigManager: PrivacyConfigurationManaging) -> Result { let database = HistoryDatabase.make() @@ -251,8 +219,6 @@ extension HistoryManager { let dbCoordinator = HistoryCoordinator(historyStoring: HistoryStore(context: context, eventMapper: HistoryStoreEventMapper())) let historyManager = HistoryManager(privacyConfigManager: privacyConfigManager, - variantManager: DefaultVariantManager(), - internalUserDecider: internalUserDecider, dbCoordinator: dbCoordinator, isAutocompleteEnabledByUser: isAutocompleteEnabledByUser(), isRecentlyVisitedSitesEnabledByUser: isRecentlyVisitedSitesEnabledByUser()) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index ec895f2abc..69dc0b2f90 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -114,7 +114,7 @@ import WebKit } } - // swiftlint:disable:next function_body_length cyclomatic_complexity + // swiftlint:disable:next function_body_length func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // SKAD4 support @@ -237,10 +237,8 @@ import WebKit variantManager.assignVariantIfNeeded { _ in // MARK: perform first time launch logic here DaxDialogs.shared.primeForUse() - historyMessageManager.dismiss() - } - if variantManager.isSupported(feature: .history) { + // New users don't see the message historyMessageManager.dismiss() } @@ -380,7 +378,6 @@ import WebKit switch HistoryManager.make(isAutocompleteEnabledByUser: settings.autocomplete, isRecentlyVisitedSitesEnabledByUser: settings.recentlyVisitedSites, - internalUserDecider: AppDependencyProvider.shared.internalUserDecider, privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) { case .failure(let error): diff --git a/DuckDuckGoTests/HistoryManagerTests.swift b/DuckDuckGoTests/HistoryManagerTests.swift index 74414ae853..8ce0afd4ee 100644 --- a/DuckDuckGoTests/HistoryManagerTests.swift +++ b/DuckDuckGoTests/HistoryManagerTests.swift @@ -28,73 +28,43 @@ final class HistoryManagerTests: XCTestCase { let privacyConfig = MockPrivacyConfiguration() let privacyConfigManager = MockPrivacyConfigurationManager() - var variantManager = MockVariantManager() - let internalUserStore = MockInternalUserStoring() - - func test() { - - struct Condition { - - let privacyConfig: Bool - let variant: Bool - let inRollOut: Bool - let internalUser: Bool - let expected: Bool + func testWhenEnabledInPrivacyConfig_ThenFeatureIsEnabled() { + privacyConfig.isFeatureKeyEnabled = { feature, _ in + XCTAssertEqual(feature, .history) + return true } - let conditions = [ - // Users in the experiment should get the feature - Condition(privacyConfig: true, variant: true, inRollOut: false, internalUser: false, expected: true), - Condition(privacyConfig: true, variant: true, inRollOut: true, internalUser: false, expected: true), - - // If not previously in the experiment then check for the rollout - Condition(privacyConfig: true, variant: false, inRollOut: false, internalUser: false, expected: false), - Condition(privacyConfig: true, variant: false, inRollOut: true, internalUser: false, expected: true), - - // Internal users also get the feature - Condition(privacyConfig: true, variant: false, inRollOut: false, internalUser: true, expected: true), - Condition(privacyConfig: true, variant: false, inRollOut: true, internalUser: true, expected: true), - - // Privacy config is the ultimate on/off switch though - Condition(privacyConfig: false, variant: true, inRollOut: true, internalUser: true, expected: false), - ] - - for index in conditions.indices { - let condition = conditions[index] - privacyConfig.isFeatureKeyEnabled = { feature, _ in - XCTAssertEqual(feature, .history) - return condition.privacyConfig - } - - privacyConfig.isSubfeatureKeyEnabled = { subFeature, _ in - XCTAssertEqual(subFeature as? HistorySubFeature, HistorySubFeature.onByDefault) - return condition.inRollOut - } - - internalUserStore.isInternalUser = condition.internalUser - privacyConfigManager.privacyConfig = privacyConfig - variantManager.isSupportedReturns = condition.variant + let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! + let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) + db.loadStore() - let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! - let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) - db.loadStore() + let historyManager = makeHistoryManager(db) { + XCTFail("DB Error \($0)") + } - let historyManager = makeHistoryManager(db) { - XCTFail("DB Error \($0)") - } + XCTAssertTrue(historyManager.isHistoryFeatureEnabled()) + XCTAssertTrue(historyManager.historyCoordinator is HistoryCoordinator) + } - let result = historyManager.isHistoryFeatureEnabled() - XCTAssertEqual(condition.expected, result, "\(index): \(condition)") + func testWhenDisabledInPrivacyConfig_ThenFeatureIsDisabled() { + privacyConfig.isFeatureKeyEnabled = { feature, _ in + XCTAssertEqual(feature, .history) + return false + } + + privacyConfigManager.privacyConfig = privacyConfig - if condition.expected { - XCTAssertTrue(historyManager.historyCoordinator is HistoryCoordinator) - } else { - XCTAssertTrue(historyManager.historyCoordinator is NullHistoryCoordinator) - } + let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! + let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) + db.loadStore() + let historyManager = makeHistoryManager(db) { + XCTFail("DB Error \($0)") } + XCTAssertFalse(historyManager.isHistoryFeatureEnabled()) + XCTAssertTrue(historyManager.historyCoordinator is NullHistoryCoordinator) } func test_WhenUserHasDisabledAutocompleteSitesSetting_ThenDontStoreOrLoadHistory() { @@ -104,7 +74,6 @@ final class HistoryManagerTests: XCTestCase { return true } - internalUserStore.isInternalUser = true privacyConfigManager.privacyConfig = privacyConfig autocompleteEnabledByUser = false @@ -126,7 +95,6 @@ final class HistoryManagerTests: XCTestCase { return true } - internalUserStore.isInternalUser = true privacyConfigManager.privacyConfig = privacyConfig recentlyVisitedSitesEnabledByUser = false @@ -147,8 +115,6 @@ final class HistoryManagerTests: XCTestCase { let dbCoordinator = HistoryCoordinator(historyStoring: store) return HistoryManager(privacyConfigManager: privacyConfigManager, - variantManager: variantManager, - internalUserDecider: DefaultInternalUserDecider(mockedStore: internalUserStore), dbCoordinator: dbCoordinator, isAutocompleteEnabledByUser: self.autocompleteEnabledByUser, isRecentlyVisitedSitesEnabledByUser: self.recentlyVisitedSitesEnabledByUser) From ab0bb3d330435e31345f54027fdbfe5f5b111c16 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 12 Jul 2024 15:53:28 +0200 Subject: [PATCH 18/48] Add desktop specific RMF attributes (#3062) Task/Issue URL: https://app.asana.com/0/72649045549333/1207774753650441/f Description: This change bumps BSK to the version containing extra matching attributes that are desktop-only. There are no functional changes to the iOS app. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 10f2560412..340cef675a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9946,7 +9946,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 170.0.0; + version = 171.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a2b8213dba..06e50aefee 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "33ceded0295158678da10d8ed685e64d3ad1a0d6", - "version" : "170.0.0" + "revision" : "9ee9b378060b94aeafba65c62e629953fec91093", + "version" : "171.0.0" } }, { From 01ebb16d4935fb9c6a1a4e8cf673206f7eb2bbeb Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Sun, 14 Jul 2024 20:59:10 +1000 Subject: [PATCH 19/48] Onboarding Intro Experiment (#3074) --- Core/DefaultVariantManager.swift | 4 + Core/PixelEvent.swift | 8 + Core/TimerInterface.swift | 42 ++++ Core/URLOpener.swift | 33 +++ Core/UniquePixel.swift | 4 +- DuckDuckGo.xcodeproj/project.pbxproj | 172 +++++++++++++++ DuckDuckGo/AnimatableTypingText.swift | 153 +++++++++++++ .../Check-Recolorable-24.pdf | Bin 0 -> 1565 bytes .../CheckGreen.imageset/Contents.json | 12 ++ .../Cross.imageset/Contents.json | 12 ++ .../Cross.imageset/Cross-Gray-24.pdf | Bin 0 -> 1008 bytes .../DDGBrowserIcon.imageset/Contents.json | 12 ++ .../DDGBrowserIcon.pdf | Bin 0 -> 20922 bytes .../DDGDefaultBrowser.imageset/Contents.json | 22 ++ .../DDGDefaultBrowser.pdf | Bin 0 -> 27577 bytes .../DDGDefaultBrowserDark.pdf | Bin 0 -> 27889 bytes .../DaxIcon.imageset/Contents.json | 2 +- .../DaxIcon.imageset/DaxLogo.pdf | Bin 0 -> 4632 bytes .../DaxIcon.imageset/new_dax_dialogs.pdf | Bin 39580 -> 0 bytes .../Hiker.imageset/Contents.json | 16 ++ .../Hiker.imageset/Hiker.pdf | Bin 0 -> 17622 bytes .../Hiker.imageset/HikerLarge.pdf | Bin 0 -> 17740 bytes .../HikerSmall.imageset/Contents.json | 12 ++ .../HikerSmall.imageset/HikerSmall.pdf | Bin 0 -> 17433 bytes .../Contents.json | 12 ++ .../OnboardingBackground.svg | 31 +++ .../SafariBrowserIcon.imageset/Contents.json | 12 ++ .../SafariBrowserIcon.pdf | Bin 0 -> 36296 bytes .../Stop.imageset/Contents.json | 12 ++ .../Stop.imageset/Stop-Yellow-24.svg | 4 + DuckDuckGo/DaxOnboardingViewController.swift | 14 ++ DuckDuckGo/Debug.storyboard | 27 ++- DuckDuckGo/MainViewController+Segues.swift | 20 +- DuckDuckGo/OnboardingButtonsView.swift | 12 +- ...boardingDefaultBroswerViewController.swift | 2 + .../BrowsersComparisonChart.swift | 128 +++++++++++ .../BrowsersComparisonModel.swift | 154 ++++++++++++++ .../DaxDialogBrowsersComparisonView.swift | 84 ++++++++ .../DaxDialogs/DaxDialogView.swift | 188 ++++++++++++++++ .../MetricBuilder/MetricBuilder.swift | 94 ++++++++ .../OnboardingBackground.swift | 106 +++++++++ .../OnboardingDefaultBrowserView.swift | 80 +++++++ .../OnboardingIntroViewController.swift | 49 +++++ .../OnboardingIntroViewModel.swift | 57 +++++ ...ardingView+BrowsersComparisonContent.swift | 69 ++++++ .../OnboardingView+IntroDialogContent.swift | 56 +++++ .../OnboardingView+Landing.swift | 121 +++++++++++ .../OnboardingIntro/OnboardingView.swift | 201 ++++++++++++++++++ .../Pixels/OnboardingPixelReporter.swift | 90 ++++++++ .../Styles/DaxDialogStyles.swift | 46 ++++ .../Styles/OnboardingTextStyles.swift | 52 +++++ .../RootDebugViewController+Onboarding.swift | 39 ++++ DuckDuckGo/RootDebugViewController.swift | 3 + DuckDuckGo/UserText.swift | 25 +++ DuckDuckGo/ViewVisibility.swift | 44 ++++ DuckDuckGo/bg.lproj/Localizable.strings | 30 +++ DuckDuckGo/cs.lproj/Localizable.strings | 30 +++ DuckDuckGo/da.lproj/Localizable.strings | 30 +++ DuckDuckGo/de.lproj/Localizable.strings | 30 +++ DuckDuckGo/el.lproj/Localizable.strings | 30 +++ DuckDuckGo/en.lproj/Localizable.strings | 30 +++ DuckDuckGo/es.lproj/Localizable.strings | 30 +++ DuckDuckGo/et.lproj/Localizable.strings | 30 +++ DuckDuckGo/fi.lproj/Localizable.strings | 30 +++ DuckDuckGo/fr.lproj/Localizable.strings | 30 +++ DuckDuckGo/hr.lproj/Localizable.strings | 30 +++ DuckDuckGo/hu.lproj/Localizable.strings | 30 +++ DuckDuckGo/it.lproj/Localizable.strings | 30 +++ DuckDuckGo/lt.lproj/Localizable.strings | 30 +++ DuckDuckGo/lv.lproj/Localizable.strings | 30 +++ DuckDuckGo/nb.lproj/Localizable.strings | 30 +++ DuckDuckGo/nl.lproj/Localizable.strings | 30 +++ DuckDuckGo/pl.lproj/Localizable.strings | 30 +++ DuckDuckGo/pt.lproj/Localizable.strings | 30 +++ DuckDuckGo/ro.lproj/Localizable.strings | 30 +++ DuckDuckGo/ru.lproj/Localizable.strings | 30 +++ DuckDuckGo/sk.lproj/Localizable.strings | 30 +++ DuckDuckGo/sl.lproj/Localizable.strings | 30 +++ DuckDuckGo/sv.lproj/Localizable.strings | 30 +++ DuckDuckGo/tr.lproj/Localizable.strings | 30 +++ .../AnimatableTypingTextModelTests.swift | 188 ++++++++++++++++ DuckDuckGoTests/MockTimer.swift | 63 ++++++ DuckDuckGoTests/MockURLOpener.swift | 41 ++++ DuckDuckGoTests/OnboardingFirePixelMock.swift | 64 ++++++ .../OnboardingIntroViewModelTests.swift | 168 +++++++++++++++ .../OnboardingPixelReporterTests.swift | 96 +++++++++ .../DuckUI/Sources/DuckUI/Button.swift | 8 +- 87 files changed, 3688 insertions(+), 26 deletions(-) create mode 100644 Core/TimerInterface.swift create mode 100644 Core/URLOpener.swift create mode 100644 DuckDuckGo/AnimatableTypingText.swift create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/CheckGreen.imageset/Check-Recolorable-24.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/CheckGreen.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Cross.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Cross.imageset/Cross-Gray-24.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/DDGBrowserIcon.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowser.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowserDark.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/DaxLogo.pdf delete mode 100644 DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/new_dax_dialogs.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Hiker.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/HikerLarge.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/HikerSmall.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/OnboardingBackground.svg create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/SafariBrowserIcon.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Stop-Yellow-24.svg create mode 100644 DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonChart.swift create mode 100644 DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift create mode 100644 DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogBrowsersComparisonView.swift create mode 100644 DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogView.swift create mode 100644 DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewController.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+Landing.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift create mode 100644 DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift create mode 100644 DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift create mode 100644 DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift create mode 100644 DuckDuckGo/RootDebugViewController+Onboarding.swift create mode 100644 DuckDuckGo/ViewVisibility.swift create mode 100644 DuckDuckGoTests/AnimatableTypingTextModelTests.swift create mode 100644 DuckDuckGoTests/MockTimer.swift create mode 100644 DuckDuckGoTests/MockURLOpener.swift create mode 100644 DuckDuckGoTests/OnboardingFirePixelMock.swift create mode 100644 DuckDuckGoTests/OnboardingIntroViewModelTests.swift create mode 100644 DuckDuckGoTests/OnboardingPixelReporterTests.swift diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index c8018ff0c2..588afe4e17 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -26,6 +26,7 @@ extension FeatureName { // Define your feature e.g.: // public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature") + public static let newOnboardingIntro = FeatureName(rawValue: "newOnboardingIntro") } public struct VariantIOS: Variant { @@ -61,6 +62,9 @@ public struct VariantIOS: Variant { VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []), + VariantIOS(name: "ma", weight: 1, isIncluded: When.always, features: []), + VariantIOS(name: "mb", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]), + returningUser ] diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index a3287ee7f8..28fd27f3ba 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -139,6 +139,10 @@ extension Pixel { case brokenSiteReport + case onboardingIntroShownUnique + case onboardingIntroComparisonChartShownUnique + case onboardingIntroChooseBrowserCTAPressed + case daxDialogsSerp case daxDialogsWithoutTrackers case daxDialogsWithoutTrackersFollowUp @@ -876,6 +880,10 @@ extension Pixel.Event { case .brokenSiteReport: return "epbf" + case .onboardingIntroShownUnique: return "m_preonboarding_intro_shown_unique" + case .onboardingIntroComparisonChartShownUnique: return "m_preonboarding_comparison_chart_shown_unique" + case .onboardingIntroChooseBrowserCTAPressed: return "m_preonboarding_choose_browser_pressed" + case .daxDialogsSerp: return "m_dx_s" case .daxDialogsWithoutTrackers: return "m_dx_wo" case .daxDialogsWithoutTrackersFollowUp: return "m_dx_wof" diff --git a/Core/TimerInterface.swift b/Core/TimerInterface.swift new file mode 100644 index 0000000000..0fa36283f5 --- /dev/null +++ b/Core/TimerInterface.swift @@ -0,0 +1,42 @@ +// +// TimerInterface.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public protocol TimerInterface: AnyObject { + var isValid: Bool { get } + func invalidate() + func fire() +} + +extension Timer: TimerInterface {} + +public protocol TimerCreating: AnyObject { + func makeTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (TimerInterface) -> Void) -> TimerInterface +} + +public final class TimerFactory: TimerCreating { + + public init() {} + + public func makeTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (TimerInterface) -> Void) -> TimerInterface { + Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats, block: block) + } + +} diff --git a/Core/URLOpener.swift b/Core/URLOpener.swift new file mode 100644 index 0000000000..d107c8a805 --- /dev/null +++ b/Core/URLOpener.swift @@ -0,0 +1,33 @@ +// +// URLOpener.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +public protocol URLOpener: AnyObject { + func canOpenURL(_ url: URL) -> Bool + func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) +} + +public extension URLOpener { + func open(_ url: URL) { + open(url, options: [:], completionHandler: nil) + } +} + +extension UIApplication: URLOpener {} diff --git a/Core/UniquePixel.swift b/Core/UniquePixel.swift index 5767d91a83..f96d4c54b0 100644 --- a/Core/UniquePixel.swift +++ b/Core/UniquePixel.swift @@ -54,8 +54,8 @@ public final class UniquePixel { withAdditionalParameters params: [String: String] = [:], includedParameters: [Pixel.QueryParameters] = [.appVersion], onComplete: @escaping (Swift.Error?) -> Void = { _ in }) { - guard pixel.name.hasSuffix("_u") else { - assertionFailure("Unique pixel: must end with _u") + guard pixel.name.hasSuffix("_u") || pixel.name.hasSuffix("_unique") else { + assertionFailure("Unique pixel: must end with _u or _unique") return } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 340cef675a..99564b7563 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -609,8 +609,33 @@ 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; + 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; + 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; + 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; + 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; }; 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; + 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */; }; + 9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */; }; 9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; }; + 9FB027122C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */; }; + 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */; }; + 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */; }; + 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */; }; + 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */; }; + 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */; }; + 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */; }; + 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */; }; + 9FE08BD62C2A60CD001D5EBC /* MetricBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD52C2A60CD001D5EBC /* MetricBuilder.swift */; }; + 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */; }; + 9FE08BDC2C2A88FA001D5EBC /* OnboardingIntroViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BDB2C2A88FA001D5EBC /* OnboardingIntroViewController.swift */; }; + 9FEA22272C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA22262C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift */; }; + 9FEA22292C2E38FA006B03BF /* AnimatableTypingText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA22282C2E38FA006B03BF /* AnimatableTypingText.swift */; }; + 9FEA222E2C324ECD006B03BF /* ViewVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA222D2C324ECD006B03BF /* ViewVisibility.swift */; }; + 9FEA22302C325125006B03BF /* AnimatableTypingTextModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA222F2C325125006B03BF /* AnimatableTypingTextModelTests.swift */; }; + 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA22312C3270BD006B03BF /* TimerInterface.swift */; }; + 9FEA22352C327226006B03BF /* MockTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA22332C3271DC006B03BF /* MockTimer.swift */; }; + 9FF7E9822C22A1F100902BE5 /* DaxDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF7E9812C22A1F100902BE5 /* DaxDialogView.swift */; }; + 9FF7E9862C23D10300902BE5 /* BrowsersComparisonChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF7E9852C23D10300902BE5 /* BrowsersComparisonChart.swift */; }; AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */; }; AA3D854723D9E88E00788410 /* AppIconSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854623D9E88E00788410 /* AppIconSettingsCell.swift */; }; AA3D854923DA1DFB00788410 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854823DA1DFB00788410 /* AppIcon.swift */; }; @@ -2250,7 +2275,32 @@ 98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = ""; }; 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = ""; }; + 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; + 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; + 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; + 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = ""; }; + 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFirePixelMock.swift; sourceTree = ""; }; + 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Landing.swift"; sourceTree = ""; }; 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = ""; }; + 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+IntroDialogContent.swift"; sourceTree = ""; }; + 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+BrowsersComparisonContent.swift"; sourceTree = ""; }; + 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsersComparisonModel.swift; sourceTree = ""; }; + 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModel.swift; sourceTree = ""; }; + 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporter.swift; sourceTree = ""; }; + 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterTests.swift; sourceTree = ""; }; + 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTextStyles.swift; sourceTree = ""; }; + 9FE08BD52C2A60CD001D5EBC /* MetricBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricBuilder.swift; sourceTree = ""; }; + 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLOpener.swift; sourceTree = ""; }; + 9FE08BDB2C2A88FA001D5EBC /* OnboardingIntroViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewController.swift; sourceTree = ""; }; + 9FEA22262C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RootDebugViewController+Onboarding.swift"; sourceTree = ""; }; + 9FEA22282C2E38FA006B03BF /* AnimatableTypingText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatableTypingText.swift; sourceTree = ""; }; + 9FEA222D2C324ECD006B03BF /* ViewVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewVisibility.swift; sourceTree = ""; }; + 9FEA222F2C325125006B03BF /* AnimatableTypingTextModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatableTypingTextModelTests.swift; sourceTree = ""; }; + 9FEA22312C3270BD006B03BF /* TimerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerInterface.swift; sourceTree = ""; }; + 9FEA22332C3271DC006B03BF /* MockTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTimer.swift; sourceTree = ""; }; + 9FF7E9812C22A1F100902BE5 /* DaxDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogView.swift; sourceTree = ""; }; + 9FF7E9852C23D10300902BE5 /* BrowsersComparisonChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsersComparisonChart.swift; sourceTree = ""; }; AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSettingsViewController.swift; sourceTree = ""; }; AA3D854623D9E88E00788410 /* AppIconSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSettingsCell.swift; sourceTree = ""; }; AA3D854823DA1DFB00788410 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; @@ -2919,6 +2969,8 @@ EE4BE0082A740BED00CD6AA8 /* ClearTextField.swift */, CB825C912C071B1400BCC586 /* AlertView.swift */, CB825C952C071C9300BCC586 /* AlertViewPresenter.swift */, + 9FEA222D2C324ECD006B03BF /* ViewVisibility.swift */, + 9FEA22282C2E38FA006B03BF /* AnimatableTypingText.swift */, ); name = SwiftUI; sourceTree = ""; @@ -3875,6 +3927,7 @@ 858566FA252E55D6007501B8 /* ImageCacheDebugViewController.swift */, 8590CB66268A2E520089F6BF /* RootDebugViewController.swift */, F1D43AFB2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift */, + 9FEA22262C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift */, 8590CB68268A4E190089F6BF /* DebugEtagStorage.swift */, 1EDE39D12705D4A100C99C72 /* FileSizeDebugViewController.swift */, 983D71B02A286E810072E26D /* SyncDebugViewController.swift */, @@ -4204,6 +4257,93 @@ name = Themes; sourceTree = ""; }; + 9F23B7FF2C2BABE000950875 /* OnboardingIntro */ = { + isa = PBXGroup; + children = ( + 9FE08BDB2C2A88FA001D5EBC /* OnboardingIntroViewController.swift */, + 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */, + 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */, + 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */, + 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */, + 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */, + ); + path = OnboardingIntro; + sourceTree = ""; + }; + 9F23B8042C2BE20500950875 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 9F9EE4CB2C377D2400D4118E /* Mocks */, + 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */, + 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */, + ); + name = Onboarding; + sourceTree = ""; + }; + 9F9EE4CB2C377D2400D4118E /* Mocks */ = { + isa = PBXGroup; + children = ( + 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */, + ); + name = Mocks; + sourceTree = ""; + }; + 9FB027102C2526A8009EA190 /* DaxDialogs */ = { + isa = PBXGroup; + children = ( + 9FF7E9812C22A1F100902BE5 /* DaxDialogView.swift */, + ); + path = DaxDialogs; + sourceTree = ""; + }; + 9FB027172C26BC0F009EA190 /* BrowsersComparison */ = { + isa = PBXGroup; + children = ( + 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */, + 9FF7E9852C23D10300902BE5 /* BrowsersComparisonChart.swift */, + ); + path = BrowsersComparison; + sourceTree = ""; + }; + 9FE05CEC2C36423C00D9046B /* Pixels */ = { + isa = PBXGroup; + children = ( + 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */, + ); + path = Pixels; + sourceTree = ""; + }; + 9FE08BD12C2A5B77001D5EBC /* Styles */ = { + isa = PBXGroup; + children = ( + 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */, + 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */, + ); + path = Styles; + sourceTree = ""; + }; + 9FE08BD42C2A60BD001D5EBC /* MetricBuilder */ = { + isa = PBXGroup; + children = ( + 9FE08BD52C2A60CD001D5EBC /* MetricBuilder.swift */, + ); + path = MetricBuilder; + sourceTree = ""; + }; + 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */ = { + isa = PBXGroup; + children = ( + 9FE05CEC2C36423C00D9046B /* Pixels */, + 9FE08BD42C2A60BD001D5EBC /* MetricBuilder */, + 9FE08BD12C2A5B77001D5EBC /* Styles */, + 9FB027172C26BC0F009EA190 /* BrowsersComparison */, + 9FB027102C2526A8009EA190 /* DaxDialogs */, + 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, + 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, + ); + path = OnboardingExperiment; + sourceTree = ""; + }; AA4D6A8023DE4973007E8790 /* AppIcon */ = { isa = PBXGroup; children = ( @@ -4935,6 +5075,7 @@ 851DFD88212C5ED600D95F20 /* Main */, EE56DE3A2A6038F500375C41 /* NetworkProtection */, F1D477C71F2139210031ED49 /* OmniBar */, + 9F23B8042C2BE20500950875 /* Onboarding */, 98EA2C3F218BB5140023E1DC /* Settings */, F1BDDBFC2C340D9C00459306 /* Subscription */, 569437222BDD402600C0881B /* Sync */, @@ -5108,6 +5249,8 @@ 1EE411F22857C4A30003FE64 /* CollectionExtension.swift */, 1E6A4D682984208800A371D3 /* LocaleExtension.swift */, 56D855692BEA9169009F9698 /* CurrentDateProviding.swift */, + 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */, + 9FEA22312C3270BD006B03BF /* TimerInterface.swift */, ); name = Utilities; sourceTree = ""; @@ -5185,6 +5328,8 @@ C1B0F6412AB08BE9001EAF05 /* MockPrivacyConfiguration.swift */, C185ED652BD43A5500BAE9DC /* MockDDGSyncing.swift */, 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */, + 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */, + 9FEA22332C3271DC006B03BF /* MockTimer.swift */, ); name = Mocks; sourceTree = ""; @@ -5233,6 +5378,7 @@ children = ( F1E092C01E92A72E00732CCC /* UIColorExtensionTests.swift */, F194FAFA1F14E622009B4DF8 /* UIFontExtensionTests.swift */, + 9FEA222F2C325125006B03BF /* AnimatableTypingTextModelTests.swift */, ); name = UserInterface; sourceTree = ""; @@ -5319,6 +5465,7 @@ F1BE54481E69DD5F00FCF649 /* Onboarding */ = { isa = PBXGroup; children = ( + 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */, 984147AA24F0259000362052 /* Onboarding.storyboard */, 851B128722200575004781BC /* Onboarding.swift */, F47E53D8250A97330037C686 /* OnboardingDefaultBroswerViewController.swift */, @@ -6539,10 +6686,12 @@ AA3D854923DA1DFB00788410 /* AppIcon.swift in Sources */, D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */, 8590CB612684D0600089F6BF /* CookieDebugViewController.swift in Sources */, + 9FEA22272C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift in Sources */, 319A37152829A55F0079FBCE /* AutofillListItemTableViewCell.swift in Sources */, 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */, 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */, 854A01332A558B3A00FCC628 /* UIView+Constraints.swift in Sources */, + 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */, C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */, 83134D7D20E2D725006CE65D /* FeedbackSender.swift in Sources */, B652DF12287C336E00C12A9C /* ContentBlockingUpdating.swift in Sources */, @@ -6558,6 +6707,7 @@ F1F5337C1F26A9EF00D80D4F /* UserText.swift in Sources */, 6FDB3F192BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift in Sources */, 1E8AD1C727BE9B2900ABA377 /* DownloadsListDataSource.swift in Sources */, + 9FE08BDC2C2A88FA001D5EBC /* OnboardingIntroViewController.swift in Sources */, 3157B43527F497F50042D3D7 /* SaveLoginViewController.swift in Sources */, C14D43012B45D6CD00ACA4DC /* AutofillDebugViewController.swift in Sources */, 853C5F6121C277C7001F7A05 /* global.swift in Sources */, @@ -6590,6 +6740,8 @@ 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, + 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */, + 9FE08BD62C2A60CD001D5EBC /* MetricBuilder.swift in Sources */, 1E1626072968413B0004127F /* ViewExtension.swift in Sources */, 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, @@ -6620,9 +6772,11 @@ F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */, 6FE1273A2C204BD000EB5724 /* NewTabPageView.swift in Sources */, 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, + 9FF7E9822C22A1F100902BE5 /* DaxDialogView.swift in Sources */, 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, C1641EB32BC2F53C0012607A /* ImportPasswordsViewModel.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, + 9FEA22292C2E38FA006B03BF /* AnimatableTypingText.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */, D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */, @@ -6660,6 +6814,7 @@ 83BE9BC3215D69C1009844D9 /* AppConfigurationFetch.swift in Sources */, 37CF91622BB474AA00BADCAE /* CrashCollectionOnboardingView.swift in Sources */, 1EEC460627A9499600E75FCB /* DownloadsList.swift in Sources */, + 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */, C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, 85B9CB8921AEBDD5009001F1 /* FavoriteHomeCell.swift in Sources */, @@ -6700,6 +6855,7 @@ 981FED6E22025151008488D7 /* BlankSnapshotViewController.swift in Sources */, D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */, 851B128822200575004781BC /* Onboarding.swift in Sources */, + 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */, 3151F0EE2735800800226F58 /* VoiceSearchFeedbackView.swift in Sources */, 37CF91642BB4A82A00BADCAE /* CrashCollectionOnboardingViewModel.swift in Sources */, 857EEB752095FFAC008A005C /* HomeRowInstructionsViewController.swift in Sources */, @@ -6714,6 +6870,7 @@ 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckNavigationHandling.swift in Sources */, + 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, 1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */, 85374D3C21AC41E700FF5A1E /* FavoritesHomeViewSectionRenderer.swift in Sources */, @@ -6795,6 +6952,7 @@ 1DDF40292BA04FCD006850D9 /* SettingsPrivacyProtectionsView.swift in Sources */, F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */, 85F2FFCD2211F615006BB258 /* MainViewController+KeyCommands.swift in Sources */, + 9FF7E9862C23D10300902BE5 /* BrowsersComparisonChart.swift in Sources */, 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */, 858650D9246B0D3C00C36F8A /* DaxOnboardingViewController.swift in Sources */, 312E5746283BB04A00C18FA0 /* AutofillEmptySearchView.swift in Sources */, @@ -6813,6 +6971,7 @@ 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 6FB2A6802C2EA950004D20C8 /* FavoritesModel.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, + 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, 310C4B47281B60E300BA79A9 /* AutofillLoginDetailsViewModel.swift in Sources */, 85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */, @@ -6833,6 +6992,7 @@ 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */, 980891A32237146B00313A70 /* Feedback.swift in Sources */, F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */, + 9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */, 85058369219F424500ED4EDB /* UIColorExtension.swift in Sources */, D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */, 85058368219C49E000ED4EDB /* HomeViewSectionRenderers.swift in Sources */, @@ -6885,6 +7045,7 @@ 310D091D2799F57200DC0060 /* Download.swift in Sources */, C13F3F6C2B7F88470083BE40 /* AuthConfirmationPromptViewModel.swift in Sources */, 1EEF124E2850EADE003DDE57 /* PrivacyIconView.swift in Sources */, + 9FB027122C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift in Sources */, 37FCAAAB29911BF1000E420A /* WaitlistExtensions.swift in Sources */, EE4BE0092A740BED00CD6AA8 /* ClearTextField.swift in Sources */, F44D279C27F331BB0037F371 /* AutofillLoginPromptView.swift in Sources */, @@ -6944,10 +7105,13 @@ F1D796F41E7C2A410019D451 /* BookmarksDelegate.swift in Sources */, D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */, 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, + 9FEA222E2C324ECD006B03BF /* ViewVisibility.swift in Sources */, 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */, D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, + 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */, + 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */, F17669D71E43401C003D3222 /* MainViewController.swift in Sources */, 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */, 984D60B2222A1284003B9E3B /* FeedbackFormViewController.swift in Sources */, @@ -6984,6 +7148,7 @@ 83EDCC411F86B89C005CDFCD /* StatisticsLoaderTests.swift in Sources */, C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */, 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */, + 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */, 85BA58581F34F72F00C6E8CA /* AppUserDefaultsTests.swift in Sources */, F1134EBC1F40D45700B73467 /* MockStatisticsStore.swift in Sources */, 983C52E72C2C0ACB007B5747 /* BookmarkStateRepairTests.swift in Sources */, @@ -7038,6 +7203,7 @@ 987130C5294AAB9F00AB05E0 /* BookmarkEditorViewModelTests.swift in Sources */, BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, + 9FEA22302C325125006B03BF /* AnimatableTypingTextModelTests.swift in Sources */, 8341D807212D5E8D000514C2 /* HashExtensionTest.swift in Sources */, C1D21E2F293A599C006E5A05 /* AutofillLoginSessionTests.swift in Sources */, 85D2187924BF6B8B004373D2 /* FaviconSourcesProviderTests.swift in Sources */, @@ -7050,10 +7216,12 @@ CB48D3372B90DF2000631D8B /* UserBehaviorMonitorTests.swift in Sources */, 8528AE7E212EF5FF00D0BD74 /* AppRatingPromptTests.swift in Sources */, 981FED692201FE69008488D7 /* AutoClearSettingsScreenTests.swift in Sources */, + 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */, 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */, 314A3EFC293905EC00D3D4C8 /* BrokenSiteReportingTests.swift in Sources */, 851B1283221FE65E004781BC /* ImproveOnboardingExperiment1Tests.swift in Sources */, F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */, + 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */, F40F843728C939760081AE75 /* AutofillLoginListViewModelTests.swift in Sources */, C14882E827F20DAB00D59F0C /* TestDataLoader.swift in Sources */, C14882EA27F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift in Sources */, @@ -7078,6 +7246,7 @@ C174CE602BD6A6CE00AED2EA /* MockDDGSyncing.swift in Sources */, 8521FDE6238D414B00A44CC3 /* FileStoreTests.swift in Sources */, F14E491F1E391CE900DC037C /* URLExtensionTests.swift in Sources */, + 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */, 85D2187424BF25CD004373D2 /* FaviconsTests.swift in Sources */, 85AD49EE2B6149110085D2D1 /* CookieStorageTests.swift in Sources */, 569437242BDD405400C0881B /* SyncBookmarksAdapterTests.swift in Sources */, @@ -7107,6 +7276,7 @@ 851DFD8A212C5EE800D95F20 /* TabSwitcherButtonTests.swift in Sources */, 98983096255B5019003339A2 /* BookmarksCachingSearchTests.swift in Sources */, D6B67A122C332B6E002122EB /* DuckPlayerMocks.swift in Sources */, + 9FEA22352C327226006B03BF /* MockTimer.swift in Sources */, EE7917912A83DE93008DFF28 /* CombineTestUtilities.swift in Sources */, 8540BD5223D8C2220057FDD2 /* PreserveLoginsTests.swift in Sources */, 85F200072217032E006BB258 /* AddressDisplayHelperTests.swift in Sources */, @@ -7260,6 +7430,7 @@ F1134EB01F40AC6300B73467 /* AtbParser.swift in Sources */, EE50052E29C369D300AE0773 /* FeatureFlag.swift in Sources */, BD15DB852B959CFD00821457 /* BundleExtension.swift in Sources */, + 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */, 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */, B652DF10287C2C1600C12A9C /* ContentBlocking.swift in Sources */, 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, @@ -7286,6 +7457,7 @@ 85D2187624BF6164004373D2 /* FaviconSourcesProvider.swift in Sources */, 98B000532915C46E0034BCA0 /* LegacyBookmarksStoreMigration.swift in Sources */, 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */, + 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */, 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */, EE50053029C3BA0800AE0773 /* InternalUserStore.swift in Sources */, F1D477CB1F2149C40031ED49 /* Type.swift in Sources */, diff --git a/DuckDuckGo/AnimatableTypingText.swift b/DuckDuckGo/AnimatableTypingText.swift new file mode 100644 index 0000000000..7e323f99af --- /dev/null +++ b/DuckDuckGo/AnimatableTypingText.swift @@ -0,0 +1,153 @@ +// +// AnimatableTypingText.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Core +import Combine + +// MARK: - View + +struct AnimatableTypingText: View { + private let text: String + private var startAnimating: Binding + private var onTypingFinished: (() -> Void)? + + @StateObject private var model: AnimatableTypingTextModel + + init( + _ text: String, + startAnimating: Binding = .constant(true), + onTypingFinished: (() -> Void)? = nil + ) { + self.text = text + _model = StateObject(wrappedValue: AnimatableTypingTextModel(text: text, onTypingFinished: onTypingFinished)) + self.startAnimating = startAnimating + self.onTypingFinished = onTypingFinished + } + + var body: some View { + ZStack(alignment: .topLeading) { + Text(text) + .frame(maxWidth: .infinity, alignment: .leading) + .visibility(.invisible) + + if #available(iOS 15, *) { + Text(AttributedString(model.typedAttributedText)) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + Text(model.typedAttributedText.string) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .onChange(of: startAnimating.wrappedValue, perform: { shouldAnimate in + if shouldAnimate { + model.startAnimating() + } else { + model.stopAnimating() + } + }) + .onAppear { + if startAnimating.wrappedValue { + model.startAnimating() + } + } + } +} + +// MARK: - Model + +final class AnimatableTypingTextModel: ObservableObject { + private var timer: TimerInterface? + + @Published private(set) var typedAttributedText: NSAttributedString = .init(string: "") + + private var typingIndex = 0 + private var textTypedSoFar: String = "" + private let text: String + private let onTypingFinished: (() -> Void)? + private let timerFactory: TimerCreating + + init(text: String, onTypingFinished: (() -> Void)?, timerFactory: TimerCreating = TimerFactory()) { + self.text = text + self.onTypingFinished = onTypingFinished + self.timerFactory = timerFactory + } + + func startAnimating() { + timer = timerFactory.makeTimer(withTimeInterval: 0.02, repeats: true, block: { [weak self] timer in + guard timer.isValid else { return } + self?.handleTimerEvent() + }) + } + + func stopAnimating() { + timer?.invalidate() + timer = nil + + stopTyping() + } + + deinit { + timer?.invalidate() + timer = nil + } + + private func handleTimerEvent() { + if textTypedSoFar == text { + onTypingFinished?() + stopAnimating() + return + } + + showCharacter() + } + + private func stopTyping() { + typedAttributedText = NSAttributedString(string: text) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.onTypingFinished?() + } + } + + private func showCharacter() { + + func attributedTypedString(forTypedChars typedChars: [String.Element]) -> NSAttributedString { + guard #available(iOS 15, *) else { + return NSAttributedString(string: String(typedChars)) + } + + let chars = Array(text) + let untypedChars = chars[typedChars.count ..< chars.count] + let combined = NSMutableAttributedString(string: String(typedChars)) + combined.append(NSAttributedString(string: String(untypedChars), attributes: [ + NSAttributedString.Key.foregroundColor: UIColor.clear + ])) + + return combined + } + + let chars = Array(text) + typingIndex = min(typingIndex + 1, chars.count) + let typedChars = Array(chars[0 ..< typingIndex]) + textTypedSoFar = String(typedChars) + let attributedString = attributedTypedString(forTypedChars: typedChars) + typedAttributedText = attributedString + } + +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/CheckGreen.imageset/Check-Recolorable-24.pdf b/DuckDuckGo/DaxOnboarding.xcassets/CheckGreen.imageset/Check-Recolorable-24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..626a0a32f86d8185008c740ff635c19ab25b3d2e GIT binary patch literal 1565 zcma)+O>fjN5Qgvm6?3W7A~o^%hg4OfTZ#}M%9dNjA!O5b(QX1siVDA;*V&DeZ8_k> z?&PukdS*Q1qwCA7Q;|8w1aWGaaGs1Rk3CH zW%X-aR?D{+w78jnl^y%ZM8=0fd^ApzmFDC)myB}DcqW7ADqyFRK?^vONo8PCSJq4< znPhC4Fqx6kYd;+))3naCbHYfP&?Hi_8BHpa-fT7rPON1A5KrqSl?f%QM4dH zsmalVO7_6+Ua^{e{@*maj9Y0qCzl1~l=Fl%r%QNg#59M@C}*9*9_OX!mbPF)8LcFk zHbQAmAU+7E$!6Yi;SGp2&T$8#t<=&|%+a3lk!@n$eBge#T+6w$q%*0N7_M6=7lJ31 zh4>%W2x$=mv{dGV^|(whf#R@Or7ibx24w=Llm?Mb8}C3=2-Zg*Fu7xDWE&PMGN3R5 zb~tN1moy=rDHBvMQFgaeqaKYBVoz|L&SS zNRwJ!CyFN^e_hv2-_h+?JP8>2m)}1s%CC$2Y7ahEt95bFJkc%k^1Q>P9?@itO3SKi z9^10&C_XyNz1*Uy`>Mu?xX%Qyix!NK`2o$|rHImdmA_xF;3~xkmNcHTXdbawyzsvR znJL)=r5IqB4x9pC6n(L2?hm&vK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~g0VN~v>&q}&z|Sn-+EfIpTCS@ z>@@zs87SzHs~Xbz{n*>9_aW+3WP@=b8Vzb+gt|ao?QQ z{0DLzhWi=czL*j8;<(oJ2Y3AZ6qpN2CTUcz4`hGYsB|<)xISB!Vg%kNN+-a}gU#$N9Q~Ydh)!#7=4`zh!KHHJ!xIJ=ZfuLTP zaHws1@lK)ZlbR)#yLvxh?FkaPIEyFd^%Sk&8dsl%a!s1d=KcNZbqQaE?2e$cV-42~ zt{=U9XJ$+Gr%KBkH?sepTl=+g@p;)bUv`JC{>a`c8_20Xi~GndogDLN*<6ouxt^R{ zdpPlx!fn1iFApqcHv8dweOqv%tWXB3WaC|13d#UKroC&v9N<}cifW#u8n1U%(2q~?Bgy3mV-#aq}=u`!e4}uh6 z_BiL40`(eVxW5>Z=3s$ghGG)PgT-(QK@Nj>+&Qr%F(*GARcl32Y8sb;f;kt&`yf)m z%+%D_Q~@Xq1%?I+V3tB2T*w&cP7pu{nV4E)2w4C_2UV4sDTYRKBMZ2ylA^@SoYW#N zQ0#fS0E0uLI6pU4Q$Zs$MH3Pz`a${mB?=%9g9BJUII}7h=z4GlOe`t^d&k_uoJ&>J H)!z*Oxl=_x literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/Contents.json new file mode 100644 index 0000000000..8e6b1384ef --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DDGBrowserIcon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/DDGBrowserIcon.pdf b/DuckDuckGo/DaxOnboarding.xcassets/DDGBrowserIcon.imageset/DDGBrowserIcon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b73ca6e8b4cd279c41c06a1969c9732ad86c0fc5 GIT binary patch literal 20922 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json new file mode 100644 index 0000000000..080ee7a1cd --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "DDGDefaultBrowser.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "DDGDefaultBrowserDark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowser.pdf b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowser.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5e1e9d83af58a1168a862f94f9d7c885fe4c5dcb GIT binary patch literal 27577 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowserDark.pdf b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowserDark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..96d8a877765b546fdbd16373bfec52679f2892ff GIT binary patch literal 27889 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/Contents.json index 1ccb01ec41..70de8cfe2f 100644 --- a/DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/Contents.json +++ b/DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "new_dax_dialogs.pdf", + "filename" : "DaxLogo.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/DaxLogo.pdf b/DuckDuckGo/DaxOnboarding.xcassets/DaxIcon.imageset/DaxLogo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..05bd06d83ee0954934a1384bbc335bf267582b6b GIT binary patch literal 4632 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
%UNJi!;#^)@bXJTHuRWuxX5;XXho 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 bJ0i0W>*?hU>^jnVK&$ey^uog0#s~fv;~tCX diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json new file mode 100644 index 0000000000..09319cba51 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Hiker.pdf", + "idiom" : "iphone" + }, + { + "filename" : "HikerLarge.pdf", + "idiom" : "ipad" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Hiker.pdf b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Hiker.pdf new file mode 100644 index 0000000000000000000000000000000000000000..36d5fd8d5f636460230f7b74ca0f8cb319b26938 GIT binary patch literal 17622 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/HikerLarge.pdf b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/HikerLarge.pdf new file mode 100644 index 0000000000000000000000000000000000000000..296e5696fbcfa01cfd8620e364ee47f4956d49d6 GIT binary patch literal 17740 zcmZ^}V~{R9&?Y>#ZQHi3d+al|?K8G*+qQYewr$(m=iUAB)mK~Fe^TkX(pQq|B%Myk z6~!dznHe}?V3>)Ri0qB5V2FtL_=p(AJzOMJTnt@I|4)tOe;3AvL`)3KO#dT90s{Xb z|1V(_Xa4_??Eg=sfWZH>H?=eQA3_+G{~xo2rHzZJ6A`0?&427-rpERrrvHbDQO?xP z+{J>3^*^}sf2#j1IJ-ER8rs5m0AFuU)>>=0?cD!(!nA^VWF2+7Ct+DqdV&pf0I5NM zfO`IX{UmKgCC{`L>%x!1H-TANI!tf2dmrK&Ka~Ic2>u@ae(&~u9r}GT{@y?2{d~st z`F(FP{(c?)ezp7PjGj7ud%tx(o=4r<5x!dXDNG)x_5HN_Zl${FQ|dFe+2(K}t{V5r zj~?gwy_XTTS$=cbjoNa{vE4piYcl$MJN}7_dvC>G{bo@h9R2pSF16LoGU)W7TRf2J+gbB@*K9NG*kyeDKJ2AS z0{!!7oDuhVg2bs}bJ>6-|I)r~_PAsDPpkZV^t#h$s`dEkq=&KXQ?6-sbJR7?#3u#q^i!#W+iO#rs>cro%SPwB_Sj!g_I;XVu5G`0%jvn$vmrG|sYc zebdwk`egj>Y6jg~&#a`BG5R}=@b_}`_w8Om@aHnmwdwe+T|X^^J6C^)?lsK!M1O?x z{OzvM(kSK9rZh$Q^)koj=fQYGP&1ZSr}h0X-7+0;5@)*m;?sFG^|whtjXAGKe$&*v z>$}tAHjNN%I&wrm_H&ej5bp2()a8X`58mXc!1Sm~kB#awaVZ>+0&N@nhg;EP2+M7n z@vo-m2ixYtweKfa>L&+lpW~uepL5;kfNdRX-lS4{;Ouako~E73!n^UG(ji#06WdS6 zt4WC&$5_t}uMouf^_d;+lb#|^!wi8HPtL^T)?3cRlRX9ZQ4fUX^FuG|zk%23+~n_x zlL<|CHz}S)eaMEK^D?4xb}C&`6fJ$9-CJ(XDis=F$w*q)_IYi#9b1cGLknSyA?Y4n zdQbwTK?LT?^F8(Ogl~?R*_-lZsakDuhgPi_fAs`!E~hJ`xv!^7owd#MfZ)zYzU@1F zobOyqyC3ditznehJEAf-JN1TPR6o{k9|u5hFxq0Ou-dQTc~3mELj=t9;=k6wa)*wz z+?A}xSGx6Yr(i3uZtE|NjlmZHib|I<6F;BxTIGBSJKzV5f)rjdK8iuZAz4W?Y2!tA z&phWgL`exd!yu;60bwpYSE2?AuW_DPrQaG4eetsZk7EhM4uISpRO?jLYS@#n&7&Ln zHrI&Dv&g4rs;xH9C!M#tq<+Tv$T1b>(p3Ba^og}S?~QjGq|&fGkH)Psc3C^(9&p+e zaJ@zFFxG<|0>M>v)Eq!zvO8#<7~TF+UP09uX^GJJ4mmQc$=%Ummm>etR^Bc@eimd> z0N}1@hCrjcFuI(|0l}9ETDeHg3r%a4MrM`djq%0GFI(0I>eusg?Y#S#it*bS zEPtv>v&geRq%wipWBM)q#hZ_?7rqO?xBIF-UXS!PQV;5p=0o;L>yIp16=vEyum{`? zd~n@M0Dt&R=KIjr@HHS8KzT5kdPGmNFayykl85q5GMtVdI%#fwrCyjL+qn5kO zV8OHR{;qhl(J6Q*8w+}nt0;OXAdx%hD!JO;@Cpl_*Iz^Voj}Q>n|nNRJoed*i8}*J z%aBq($e_!6roFdsCuAe!K5bgSE%c~oHKKPt9GdKFFwiE;!)FOpESP)z231D^A|3PR zGo2OZUlG^|M(8sc0RF&eYEYosxKCn}I|E2Kf}lui^+`JIve`kWann1;kORB1g~ak& z&c7adUBU;{M!JNH+gPfjrC8Q1!}?B$BCS~eY(Snb%qAx_#(j~zk%G(AYgZKL&Jj8G z)2%C#21lm>^|Ww#lLH^*xOd^5AwHUdy`?)R*%{HhNmg;_cC4PNR7xG`28*DnlYG*>{|Rs&AvtwYFOi2L21r zu-ja4u6f>x`9g`rPZjKglg zmdfcF>CRW~F7mcWfCFkbW*WPeMP~4`p)mChczoRrgKbj+IX(EOEi!<3u*IG_8ou!< zJ!&sc>5y<%A{_1HFOsxH2fk{ii>Nh{ z7|Vy!G@tji*}Tca>cAG;a4@JqNR$QJAX%0AQj!+g8M?FT1JpM#?Ae@H_aKX#e(K!v;~=A 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 Zt_L4Hl2}v%_Ku;ssi8TSs;aBM8vt>!gp~jQ literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json new file mode 100644 index 0000000000..7509061caa --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "HikerSmall.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/HikerSmall.pdf b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/HikerSmall.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ca97f45b113b9d79aa5bc18956ef4d64009ecd1c GIT binary patch literal 17433 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 WG!lzSz}_)5H!?KfQdM>JcLM+iEli34 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/Contents.json new file mode 100644 index 0000000000..5c70c1e733 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "OnboardingBackground.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/OnboardingBackground.svg b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/OnboardingBackground.svg new file mode 100644 index 0000000000..b06c23d60d --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingBackground.imageset/OnboardingBackground.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/Contents.json new file mode 100644 index 0000000000..9efa019adb --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SafariBrowserIcon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/SafariBrowserIcon.pdf b/DuckDuckGo/DaxOnboarding.xcassets/SafariBrowserIcon.imageset/SafariBrowserIcon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5811e77af9e994f94450ccb2307c637627527ef8 GIT binary patch literal 36296 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 E1JAkx3;+NC literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Contents.json new file mode 100644 index 0000000000..32fb76ad67 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Stop-Yellow-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Stop-Yellow-24.svg b/DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Stop-Yellow-24.svg new file mode 100644 index 0000000000..70b664eb60 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/Stop.imageset/Stop-Yellow-24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/DuckDuckGo/DaxOnboardingViewController.swift b/DuckDuckGo/DaxOnboardingViewController.swift index f93227f783..2d3817929e 100644 --- a/DuckDuckGo/DaxOnboardingViewController.swift +++ b/DuckDuckGo/DaxOnboardingViewController.swift @@ -52,6 +52,18 @@ class DaxOnboardingViewController: UIViewController, Onboarding { return true } + private let pixelReporting: OnboardingIntroImpressionReporting + + init?(coder: NSCoder, pixelReporting: OnboardingIntroImpressionReporting = OnboardingPixelReporter()) { + self.pixelReporting = pixelReporting + super.init(coder: coder) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -61,6 +73,8 @@ class DaxOnboardingViewController: UIViewController, Onboarding { daxDialogContainerHeight.constant = daxDialog?.calculateHeight() ?? 0 button.displayDropShadow() daxIcon.isHidden = true + + pixelReporting.trackOnboardingIntroImpression() } override func viewDidAppear(_ animated: Bool) { diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index b0188cabd4..9d62963206 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -312,15 +312,24 @@ - + - + + + + + + + + + + @@ -869,17 +878,17 @@ - + - + - + - + diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 75335b8c45..b7f515cbc1 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -31,14 +31,20 @@ extension MainViewController { func segueToDaxOnboarding() { os_log(#function, log: .generalLog, type: .debug) hideAllHighlightsIfNeeded() - let storyboard = UIStoryboard(name: "DaxOnboarding", bundle: nil) - guard let controller = storyboard.instantiateInitialViewController(creator: { coder in - DaxOnboardingViewController(coder: coder) - }) else { - assertionFailure() - return + + var controller: (Onboarding & UIViewController)? + + if DefaultVariantManager().isSupported(feature: .newOnboardingIntro) { + controller = OnboardingIntroViewController() + } else { + let storyboard = UIStoryboard(name: "DaxOnboarding", bundle: nil) + controller = storyboard.instantiateInitialViewController(creator: { coder in + DaxOnboardingViewController(coder: coder) + }) } - controller.delegate = self + + controller?.delegate = self + guard let controller else { return assertionFailure() } controller.modalPresentationStyle = .overFullScreen present(controller, animated: false) } diff --git a/DuckDuckGo/OnboardingButtonsView.swift b/DuckDuckGo/OnboardingButtonsView.swift index 259e6c8dc8..d674397f5e 100644 --- a/DuckDuckGo/OnboardingButtonsView.swift +++ b/DuckDuckGo/OnboardingButtonsView.swift @@ -51,8 +51,14 @@ struct OnboardingActions: View { extension OnboardingActions { class Model: ObservableObject { - @Published var primaryButtonTitle = "" - @Published var secondaryButtonTitle = "" - @Published var isContinueEnabled = true + @Published var primaryButtonTitle: String + @Published var secondaryButtonTitle: String + @Published var isContinueEnabled: Bool + + init(primaryButtonTitle: String = "", secondaryButtonTitle: String = "", isContinueEnabled: Bool = true) { + self.primaryButtonTitle = primaryButtonTitle + self.secondaryButtonTitle = secondaryButtonTitle + self.isContinueEnabled = isContinueEnabled + } } } diff --git a/DuckDuckGo/OnboardingDefaultBroswerViewController.swift b/DuckDuckGo/OnboardingDefaultBroswerViewController.swift index c6401b3d29..06c8695ca3 100644 --- a/DuckDuckGo/OnboardingDefaultBroswerViewController.swift +++ b/DuckDuckGo/OnboardingDefaultBroswerViewController.swift @@ -35,6 +35,8 @@ class OnboardingDefaultBroswerViewController: OnboardingContentViewController { } override func onContinuePressed(navigationHandler: @escaping () -> Void) { + Pixel.fire(pixel: .onboardingIntroChooseBrowserCTAPressed, includedParameters: [.appVersion, .atb]) + if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } diff --git a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonChart.swift b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonChart.swift new file mode 100644 index 0000000000..ea5fc0bd0d --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonChart.swift @@ -0,0 +1,128 @@ +// +// BrowsersComparisonChart.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - Chart View + +struct BrowsersComparisonChart: View { + let privacyFeatures: [BrowsersComparisonModel.PrivacyFeature] + + var body: some View { + VStack(spacing: Metrics.stackSpacing) { + Header(browsers: BrowsersComparisonModel.Browser.allCases) + .frame(height: Metrics.headerHeight) + + ForEach(privacyFeatures, id: \.type) { feature in + Row(feature: feature) + } + + } + } +} + +// MARK: - Header + +extension BrowsersComparisonChart { + + struct Header: View { + let browsers: [BrowsersComparisonModel.Browser] + + var body: some View { + HStack(alignment: .bottom) { + Spacer() + + ForEach(Array(browsers.enumerated()), id: \.offset) { index, browser in + Image(browser.image) + .frame(width: Metrics.headerImageContainerSize.width, height: Metrics.headerImageContainerSize.height) + + if index < browsers.count - 1 { + Divider() + } + } + } + } + } + +} + +// MARK: - Row + +extension BrowsersComparisonChart { + + struct Row: View { + let feature: BrowsersComparisonModel.PrivacyFeature + + var body: some View { + HStack { + Text(verbatim: feature.type.title) + .font(Metrics.font) + .foregroundColor(.primary) + .lineLimit(nil) + .lineSpacing(1) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + + Spacer() + + BrowsersSupport(browsersSupport: feature.browsersSupport) + } + .frame(maxHeight: Metrics.imageContainerSize.height) + + Divider() + } + } + +} + +// MARK: - Row + BrowsersSupport + +extension BrowsersComparisonChart.Row { + + struct BrowsersSupport: View { + let browsersSupport: [BrowsersComparisonModel.PrivacyFeature.BrowserSupport] + + var body: some View { + ForEach(Array(browsersSupport.enumerated()), id: \.offset) { index, browserSupport in + Image(browserSupport.availability.image) + .frame(width: Metrics.imageContainerSize.width) + + if index < browsersSupport.count - 1 { + Divider() + } + } + } + } + +} + +// MARK: - Metrics + +private enum Metrics { + static let stackSpacing: CGFloat = 0.0 + static let headerHeight: CGFloat = 60 + static let headerImageContainerSize = CGSize(width: 40, height: 80) + static let imageContainerSize = CGSize(width: 40.0, height: 50.0) + static let font = Font.system(size: 15.0) +} + +#Preview { + BrowsersComparisonChart(privacyFeatures: BrowsersComparisonModel.privacyFeatures) + .padding() +} diff --git a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift new file mode 100644 index 0000000000..0d60655d97 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift @@ -0,0 +1,154 @@ +// +// BrowsersComparisonModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct BrowsersComparisonModel { + + static let privacyFeatures: [PrivacyFeature] = { + PrivacyFeature.FeatureType.allCases.map { featureType in + PrivacyFeature(type: featureType, browsersSupport: browsersSupport(for: featureType)) + } + }() + + private static func browsersSupport(for feature: PrivacyFeature.FeatureType) -> [PrivacyFeature.BrowserSupport] { + Browser.allCases.map { browser in + let availability: PrivacyFeature.Availability + switch feature { + case .privateSearch: + switch browser { + case .ddg: + availability = .available + case .safari: + availability = .unavailable + } + case .blockThirdPartyTrackers: + switch browser { + case .ddg: + availability = .available + case .safari: + availability = .partiallyAvailable + } + case .blockCookiePopups: + switch browser { + case .ddg: + availability = .available + case .safari: + availability = .unavailable + } + case .blockCreepyAds: + switch browser { + case .ddg: + availability = .available + case .safari: + availability = .unavailable + } + case .eraseBrowsingData: + switch browser { + case .ddg: + availability = .available + case .safari: + availability = .unavailable + } + } + + return PrivacyFeature.BrowserSupport(browser: browser, availability: availability) + } + } + +} + +// MARK: - Browser + +extension BrowsersComparisonModel { + + enum Browser: CaseIterable { + case safari + case ddg + + var image: ImageResource { + switch self { + case .safari: .safariBrowserIcon + case .ddg: .ddgBrowserIcon + } + } + } + +} + +// MARK: - Privacy Feature + +extension BrowsersComparisonModel { + + struct PrivacyFeature { + let type: FeatureType + let browsersSupport: [BrowserSupport] + } + +} + +extension BrowsersComparisonModel.PrivacyFeature { + + struct BrowserSupport { + let browser: BrowsersComparisonModel.Browser + let availability: Availability + } + + enum FeatureType: CaseIterable { + case privateSearch + case blockThirdPartyTrackers + case blockCookiePopups + case blockCreepyAds + case eraseBrowsingData + + var title: String { + switch self { + case .privateSearch: + UserText.DaxOnboardingExperiment.BrowsersComparison.Features.privateSearch + case .blockThirdPartyTrackers: + UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers + case .blockCookiePopups: + UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups + case .blockCreepyAds: + UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds + case .eraseBrowsingData: + UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData + } + } + } + + enum Availability: Identifiable { + case available + case partiallyAvailable + case unavailable + + var id: Self { + self + } + + var image: ImageResource { + switch self { + case .available: .checkGreen + case .partiallyAvailable: .stop + case .unavailable: .cross + } + } + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogBrowsersComparisonView.swift b/DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogBrowsersComparisonView.swift new file mode 100644 index 0000000000..bc90273bd5 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogBrowsersComparisonView.swift @@ -0,0 +1,84 @@ +// +// DaxDialogBrowsersComparisonView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI + +struct DaxDialogBrowsersComparisonView: View { + + let setAsDefaultBrowserAction: () -> Void + let cancelAction: () -> Void + + @State private var showButton = false + @State private var animateText = true + + var body: some View { + DaxDialogView( + logoPosition: .top, + onTapGesture: { + withAnimation { + showButton = true + animateText = false + } + }, + content: { + VStack(spacing: 16.0) { + AnimatableTypingText(UserText.DaxOnboardingExperiment.BrowsersComparison.title, startAnimating: $animateText) { + withAnimation { + showButton = true + } + } + .foregroundColor(.primary) + .font(Font.system(size: 20, weight: .bold)) + + + VStack(spacing: 24) { + BrowsersComparisonChart(privacyFeatures: BrowsersComparisonModel.privacyFeatures) + + OnboardingActions( + viewModel: .init( + primaryButtonTitle: UserText.DaxOnboardingExperiment.BrowsersComparison.cta, + secondaryButtonTitle: UserText.onboardingSkip + ), + primaryAction: setAsDefaultBrowserAction, + secondaryAction: cancelAction + ) + + } + .visibility(showButton ? .visible : .invisible) + } + } + ) + } + +} + +// MARK: - Preview + +#Preview("Browsers Comparison - Light Mode") { + DaxDialogBrowsersComparisonView(setAsDefaultBrowserAction: {}, cancelAction: {}) + .padding() + .preferredColorScheme(.light) +} + +#Preview("Browsers Comparison - Dark Mode") { + DaxDialogBrowsersComparisonView(setAsDefaultBrowserAction: {}, cancelAction: {}) + .padding() + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogView.swift b/DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogView.swift new file mode 100644 index 0000000000..49bf14a78f --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogView.swift @@ -0,0 +1,188 @@ +// +// DaxDialogView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DesignResourcesKit + +// MARK: - Metrics + +private enum Metrics { + static let contentPadding: CGFloat = 24.0 + static let shadowRadius: CGFloat = 5.0 + static let stackSpacing: CGFloat = 8 + + enum DaxLogo { + static let size: CGFloat = 54.0 + static let horizontalPadding: CGFloat = 10 + } +} + +// MARK: - DaxDialog + +struct DaxDialogView: View { + + enum LogoPosition { + case top + case left + } + + @Environment(\.colorScheme) var colorScheme + + @State private var logoPosition: LogoPosition + private let matchLogoAnimation: (id: String, namespace: Namespace.ID) + private let showDialogBox: Binding + private let cornerRadius: CGFloat + private let arrowSize: CGSize + private let onTapGesture: (() -> Void)? + private let content: Content + + init( + logoPosition: LogoPosition, + matchLogoAnimation: (String, Namespace.ID) = ("", Namespace().wrappedValue), + showDialogBox: Binding = .constant(true), + cornerRadius: CGFloat = 16.0, + arrowSize: CGSize = .init(width: 16.0, height: 8.0), + onTapGesture: (() -> Void)? = nil, + @ViewBuilder content: () -> Content + ) { + _logoPosition = State(initialValue: logoPosition) + self.matchLogoAnimation = matchLogoAnimation + self.showDialogBox = showDialogBox + self.cornerRadius = cornerRadius + self.arrowSize = arrowSize + self.onTapGesture = onTapGesture + self.content = content() + } + + var body: some View { + Group { + switch logoPosition { + case .top: + topLogoViewContentView + case .left: + leftLogoContentView + } + } + .onTapGesture { + onTapGesture?() + } + } + + private var topLogoViewContentView: some View { + VStack(alignment: .leading, spacing: stackSpacing) { + daxLogo + .padding(.leading, Metrics.DaxLogo.horizontalPadding) + + wrappedContent + .visibility(showDialogBox.wrappedValue ? .visible : .invisible) + } + } + + private var leftLogoContentView: some View { + HStack(alignment: .top, spacing: stackSpacing) { + daxLogo + + wrappedContent + .visibility(showDialogBox.wrappedValue ? .visible : .invisible) + } + + } + + private var stackSpacing: CGFloat { + Metrics.stackSpacing + arrowSize.height + } + + private var daxLogo: some View { + Image(.daxIcon) + .resizable() + .matchedGeometryEffect(id: matchLogoAnimation.id, in: matchLogoAnimation.namespace) + .aspectRatio(contentMode: .fill) + .frame(width: Metrics.DaxLogo.size, height: Metrics.DaxLogo.size) + } + + private var wrappedContent: some View { + let backgroundColor = Color(designSystemColor: .surface) + let shadowColors: (Color, Color) = colorScheme == .light ? + (.black.opacity(0.08), .black.opacity(0.1)) : + (.black.opacity(0.20), .black.opacity(0.16)) + + return content + .padding(.all, Metrics.contentPadding) + .background(backgroundColor) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .shadow(color: shadowColors.0, radius: 16, x: 0, y: 8) + .shadow(color: shadowColors.1, radius: 6, x: 0, y: 2) + .overlay( + Triangle() + .frame(width: arrowSize.width, height: arrowSize.height) + .foregroundColor(backgroundColor) + .rotationEffect(Angle(degrees: logoPosition == .top ? 0 : -90), anchor: .bottom) + .offset(arrowOffset) + , + alignment: .topLeading + ) + } + + private var arrowOffset: CGSize { + switch logoPosition { + case .top: + let leadingOffset = Metrics.DaxLogo.horizontalPadding + Metrics.DaxLogo.size / 2 - arrowSize.width / 2 + return CGSize(width: leadingOffset, height: -arrowSize.height) + case .left: + let topOffset = Metrics.DaxLogo.size / 2 - arrowSize.width / 2 + return CGSize(width: -arrowSize.height, height: topOffset) + } + } +} + +// MARK: - Preview + +#Preview("Dax Dialog Top Logo") { + ZStack { + Color.green.ignoresSafeArea() + + DaxDialogView(logoPosition: .top) { + VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 20) { + Text(verbatim: "Hi there.") + + Text(verbatim: "Ready for a better, more private internet?") + } + } + } + .padding() + } +} + +#Preview("Dax Dialog Left Logo") { + ZStack { + Color.green.ignoresSafeArea() + + DaxDialogView(logoPosition: .left) { + VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 20) { + Text(verbatim: "Hi there.") + + Text(verbatim: "Ready for a better, more private internet?") + } + } + } + .padding() + } +} diff --git a/DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift b/DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift new file mode 100644 index 0000000000..8cef271094 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift @@ -0,0 +1,94 @@ +// +// MetricBuilder.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import class UIKit.UIScreen + +final class MetricBuilder { + private let iPhoneValue: T + private let iPadValue: T + private var iPhoneSmallScreen: T? + private var iPadPortraitValue: T? + private var iPadLandscapeValue: T? + + init(iPhone: T, iPad: T) { + iPhoneValue = iPhone + iPadValue = iPad + } + + convenience init(value: T) { + self.init(iPhone: value, iPad: value) + } + + func iPad(portrait: T, landscape: T) -> Self { + iPadPortraitValue = portrait + iPadLandscapeValue = landscape + return self + } + + func iPad(landscape: T) -> Self { + iPadLandscapeValue = landscape + return self + } + + func smallIphone(_ value: T) -> Self { + iPhoneSmallScreen = value + return self + } + + func build(v: UserInterfaceSizeClass?, h: UserInterfaceSizeClass?) -> T { + if isIPad(v: v, h: h) { + if isIpadLandscape(v: v, h: h) { + iPadLandscapeValue ?? iPadValue + } else { + iPadPortraitValue ?? iPadValue + } + } else { + if isIPhoneSmallScreen(UIScreen.main.bounds.size) { + iPhoneSmallScreen ?? iPhoneValue + } else { + iPhoneValue + } + } + } +} + +func isIphone(v: UserInterfaceSizeClass?, h: UserInterfaceSizeClass?) -> Bool { + !isIPad(v: v, h: h) +} + +func isIPhonePortrait(v: UserInterfaceSizeClass?, h: UserInterfaceSizeClass?) -> Bool { + v == .regular && h == .compact +} + +func isIPhoneLandscape(v: UserInterfaceSizeClass?) -> Bool { + v == .compact +} + +func isIPhoneSmallScreen(_ frame: CGSize) -> Bool { + frame.height > 0 && frame.height <= 667 // iPhone SE +} + +func isIPad(v: UserInterfaceSizeClass?, h: UserInterfaceSizeClass?) -> Bool { + v == .regular && h == .regular +} + +func isIpadLandscape(v: UserInterfaceSizeClass?, h: UserInterfaceSizeClass?) -> Bool { + isIPad(v: v, h: h) && UIScreen.main.bounds.width > UIScreen.main.bounds.height +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift b/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift new file mode 100644 index 0000000000..a95fb56bb0 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift @@ -0,0 +1,106 @@ +// +// OnboardingBackground.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OnboardingBackground: View { + @Environment(\.verticalSizeClass) private var vSizeClass + @Environment(\.horizontalSizeClass) private var hSizeClass + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + GeometryReader { proxy in + // On iPhone we want the background image to start from the left but on iPad we want to take the center part + let alignment = Metrics.imageCentering.build(v: vSizeClass, h: hSizeClass) + Image(.onboardingBackground) + .resizable() + .aspectRatio(contentMode: .fill) + .opacity(colorScheme == .light ? 0.5 : 0.3) + .frame(width: proxy.size.width, height: proxy.size.height, alignment: alignment) + .background( + Gradient() + .ignoresSafeArea() + ) + } + } +} + +private enum Metrics { + static let imageCentering = MetricBuilder(iPhone: .bottomLeading, iPad: .center) +} + +// MARK: - Gradient + +private extension OnboardingBackground { + + struct Gradient: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + switch colorScheme { + case .light: + lightGradient + case .dark: + darkGradient + @unknown default: + lightGradient + } + } + + private var lightGradient: some View { + gradient(colorStops: [ + .init(color: Color(red: 1, green: 0.9, blue: 0.87), location: 0.00), + .init(color: Color(red: 0.99, green: 0.89, blue: 0.87), location: 0.28), + .init(color: Color(red: 0.99, green: 0.89, blue: 0.87), location: 0.46), + .init(color: Color(red: 0.96, green: 0.87, blue: 0.87), location: 0.72), + .init(color: Color(red: 0.9, green: 0.84, blue: 0.92), location: 1.00), + ]) + } + + private var darkGradient: some View { + gradient(colorStops: [ + .init(color: Color(red: 0.29, green: 0.19, blue: 0.25), location: 0.00), + .init(color: Color(red: 0.35, green: 0.23, blue: 0.32), location: 0.28), + .init(color: Color(red: 0.37, green: 0.25, blue: 0.38), location: 0.46), + .init(color: Color(red: 0.2, green: 0.15, blue: 0.32), location: 0.72), + .init(color: Color(red: 0.16, green: 0.15, blue: 0.34), location: 1.00), + ]) + } + + private func gradient(colorStops: [SwiftUI.Gradient.Stop]) -> some View { + LinearGradient( + stops: colorStops, + startPoint: UnitPoint(x: 0.5, y: 0), + endPoint: UnitPoint(x: 0.5, y: 1) + ) + } + + } + +} + +#Preview("Light Mode") { + OnboardingBackground() + .preferredColorScheme(.light) +} + +#Preview("Dark Mode") { + OnboardingBackground() + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift new file mode 100644 index 0000000000..25f058bca9 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift @@ -0,0 +1,80 @@ +// +// OnboardingDefaultBrowserView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OnboardingDefaultBrowserView: View { + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + let setAsDefaultBrowserAction: () -> Void + let cancelAction: () -> Void + + var body: some View { + ZStack { + Color(designSystemColor: .surface) + .ignoresSafeArea() + + VStack(spacing: Metrics.verticalSpacing) { + Text(UserText.onboardingDefaultBrowserTitle) + .onboardingTitleStyle(fontSize: 28) + .padding([.top, .horizontal]) + + Text(UserText.DaxOnboardingExperiment.DefaultBrowser.message) + .font(.system(size: 16.0)) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + + Image(.ddgDefaultBrowser) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(.top, 48) + + Spacer() + .frame(height: Metrics.spacerHeight.build(v: verticalSizeClass, h: horizontalSizeClass)) + + OnboardingActions( + viewModel: .init( + primaryButtonTitle: UserText.onboardingSetAsDefaultBrowser, + secondaryButtonTitle: UserText.onboardingDefaultBrowserMaybeLater + ), + primaryAction: setAsDefaultBrowserAction, + secondaryAction: cancelAction + ) + + } + .padding(.top) + .frame(maxWidth: Metrics.viewWidth, maxHeight: .infinity, alignment: .center) + } + } +} + +// MARK: - Metrics + +private enum Metrics { + static let verticalSpacing: CGFloat = 16.0 + static let spacerHeight = MetricBuilder(iPhone: 142, iPad: 142).smallIphone(10) + static let viewWidth: CGFloat = 325.0 +} + +// MARK: - Preview + +#Preview { + OnboardingDefaultBrowserView(setAsDefaultBrowserAction: {}, cancelAction: {}) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewController.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewController.swift new file mode 100644 index 0000000000..2b78cc51d6 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewController.swift @@ -0,0 +1,49 @@ +// +// OnboardingIntroViewController.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +final class OnboardingIntroViewController: UIHostingController, Onboarding { + weak var delegate: OnboardingDelegate? + private let viewModel: OnboardingIntroViewModel + + init() { + viewModel = OnboardingIntroViewModel() + let rootView = OnboardingView(model: viewModel) + super.init(rootView: rootView) + + viewModel.onCompletingOnboardingIntro = { [weak self] in + guard let self else { return } + self.delegate?.onboardingCompleted(controller: self) + } + } + + @available(*, unavailable) + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return [.portrait] + } + + override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { + return .portrait + } +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift new file mode 100644 index 0000000000..376366ec60 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -0,0 +1,57 @@ +// +// OnboardingIntroViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core +import class UIKit.UIApplication + +final class OnboardingIntroViewModel: ObservableObject { + @Published private(set) var state: OnboardingView.ViewState = .landing + + var onCompletingOnboardingIntro: (() -> Void)? + private let pixelReporter: OnboardingIntroPixelReporting + private let urlOpener: URLOpener + + init(pixelReporter: OnboardingIntroPixelReporting = OnboardingPixelReporter(), urlOpener: URLOpener = UIApplication.shared) { + self.pixelReporter = pixelReporter + self.urlOpener = urlOpener + } + + func onAppear() { + state = .onboarding(.startOnboardingDialog) + pixelReporter.trackOnboardingIntroImpression() + } + + func startOnboardingAction() { + state = .onboarding(.browsersComparisonDialog) + pixelReporter.trackBrowserComparisonImpression() + } + + func setDefaultBrowserAction() { + if let url = URL(string: UIApplication.openSettingsURLString) { + urlOpener.open(url) + } + pixelReporter.trackChooseBrowserCTAAction() + onCompletingOnboardingIntro?() + } + + func cancelSetDefaultBrowserAction() { + onCompletingOnboardingIntro?() + } +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift new file mode 100644 index 0000000000..1bf385a1da --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift @@ -0,0 +1,69 @@ +// +// OnboardingView+BrowsersComparisonContent.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI + +extension OnboardingView { + + struct BrowsersComparisonContent: View { + + private var animateText: Binding + private var showContent: Binding + private let setAsDefaultBrowserAction: () -> Void + private let cancelAction: () -> Void + + init(animateText: Binding = .constant(true), showContent: Binding = .constant(false), setAsDefaultBrowserAction: @escaping () -> Void, cancelAction: @escaping () -> Void) { + self.animateText = animateText + self.showContent = showContent + self.setAsDefaultBrowserAction = setAsDefaultBrowserAction + self.cancelAction = cancelAction + } + + var body: some View { + VStack(spacing: 16.0) { + AnimatableTypingText(UserText.DaxOnboardingExperiment.BrowsersComparison.title, startAnimating: animateText) { + withAnimation { + showContent.wrappedValue = true + } + } + .foregroundColor(.primary) + .font(Font.system(size: 20, weight: .bold)) + + + VStack(spacing: 24) { + BrowsersComparisonChart(privacyFeatures: BrowsersComparisonModel.privacyFeatures) + + OnboardingActions( + viewModel: .init( + primaryButtonTitle: UserText.DaxOnboardingExperiment.BrowsersComparison.cta, + secondaryButtonTitle: UserText.onboardingSkip + ), + primaryAction: setAsDefaultBrowserAction, + secondaryAction: cancelAction + ) + + } + .visibility(showContent.wrappedValue ? .visible : .invisible) + } + } + + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift new file mode 100644 index 0000000000..55b8931fa0 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift @@ -0,0 +1,56 @@ +// +// OnboardingView+IntroDialogContent.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI + +extension OnboardingView { + + struct IntroDialogContent: View { + + private var animateText: Binding + private var showCTA: Binding + private let action: () -> Void + + init(animateText: Binding = .constant(true), showCTA: Binding = .constant(false), action: @escaping () -> Void) { + self.animateText = animateText + self.showCTA = showCTA + self.action = action + } + + var body: some View { + VStack(spacing: 24.0) { + AnimatableTypingText(UserText.DaxOnboardingExperiment.Intro.title, startAnimating: animateText) { + withAnimation { + showCTA.wrappedValue = true + } + } + .foregroundColor(.primary) + .font(Font.system(size: 20, weight: .bold)) + + Button(action: action) { + Text(UserText.DaxOnboardingExperiment.Intro.cta) + } + .buttonStyle(PrimaryButtonStyle()) + .visibility(showCTA.wrappedValue ? .visible : .invisible) + } + } + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+Landing.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+Landing.swift new file mode 100644 index 0000000000..379d920fcf --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+Landing.swift @@ -0,0 +1,121 @@ +// +// OnboardingView+Landing.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +extension OnboardingView { + + struct LandingView: View { + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + let animationNamespace: Namespace.ID + + var body: some View { + GeometryReader { proxy in + if isIpadLandscape(v: verticalSizeClass, h: horizontalSizeClass) { + landingScreenIPadLandscape(proxy: proxy) + } else { + landingScreenPortrait(proxy: proxy) + } + } + } + + func landingScreenPortrait(proxy: GeometryProxy) -> some View { + VStack { + Spacer() + + welcomeView + + Spacer() + + Image(Metrics.hikerImage.build(v: verticalSizeClass, h: horizontalSizeClass)) + } + .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) + } + + func landingScreenIPadLandscape(proxy: GeometryProxy) -> some View { + HStack(spacing: 0) { + // Divide screen in half with two containers: + // 1. Hiker to be centered horizontally in the container and with a height of 90% of the screen size + // 2. Welcome view horizontally centered in the container with min padding leading and trailing to wrap the text if needed. + VStack(alignment: .center) { + Image(Metrics.hikerImage.build(v: verticalSizeClass, h: horizontalSizeClass)) + .resizable() + .scaledToFit() + .frame(height: proxy.size.height * Metrics.Landscape.hikerHeightPercentage) + } + .frame(width: proxy.size.width / 2, height: proxy.size.height, alignment: .bottom) + + HStack { + Spacer(minLength: proxy.size.width / 2 * Metrics.Landscape.textMinSpacerPercentage) + + welcomeView + .padding(.top, proxy.size.height * Metrics.Landscape.daxImagePositionPercentage) + + Spacer(minLength: proxy.size.width / 2 * Metrics.Landscape.textMinSpacerPercentage) + } + .frame(width: proxy.size.width / 2, height: proxy.size.height, alignment: .top) + } + } + + private var welcomeView: some View { + let iconSize = Metrics.iconSize.build(v: verticalSizeClass, h: horizontalSizeClass) + + return VStack(alignment: .center, spacing: Metrics.welcomeMessageStackSpacing.build(v: verticalSizeClass, h: horizontalSizeClass)) { + Image(.daxIcon) + .resizable() + .matchedGeometryEffect(id: OnboardingView.daxGeometryEffectID, in: animationNamespace) + .frame(width: iconSize.width, height: iconSize.height) + + Text(UserText.onboardingWelcomeHeader) + .onboardingTitleStyle(fontSize: Metrics.titleSize.build(v: verticalSizeClass, h: horizontalSizeClass)) + .frame(width: Metrics.titleWidth.build(v: verticalSizeClass, h: horizontalSizeClass), alignment: .top) + } + } + + } +} + +// MARK: - Metrics + +private enum Metrics { + static let iconSize = MetricBuilder(value: .init(width: 70, height: 70)).iPad(landscape: .init(width: 96, height: 96)) + static let welcomeMessageStackSpacing = MetricBuilder(iPhone: 13, iPad: 32) + static let titleSize = MetricBuilder(iPhone: 28, iPad: 36).iPad(landscape: 48) + static let titleWidth = MetricBuilder(iPhone: 252, iPad: nil) + static let hikerImage = MetricBuilder(value: .hiker).smallIphone(.hikerSmall) + enum Landscape { + static let textMinSpacerPercentage: CGFloat = 0.15 + static let daxImagePositionPercentage: CGFloat = 0.15 + static let hikerHeightPercentage: CGFloat = 0.9 + } +} + +// MARK: - Preview + +#Preview("Light Mode") { + OnboardingView.LandingView(animationNamespace: Namespace().wrappedValue) + .preferredColorScheme(.light) +} + +#Preview("Dark Mode") { + OnboardingView.LandingView(animationNamespace: Namespace().wrappedValue) + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift new file mode 100644 index 0000000000..c4c3487cab --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -0,0 +1,201 @@ +// +// OnboardingView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - OnboardingView + +struct OnboardingView: View { + + static let daxGeometryEffectID = "DaxIcon" + + @Namespace var animationNamespace + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @ObservedObject private var model: OnboardingIntroViewModel + + @State private var showDaxDialogBox = false + @State private var showIntroViewContent = true + @State private var showIntroButton = false + @State private var animateIntroText = false + @State private var showComparisonButton = false + @State private var animateComparisonText = false + + init(model: OnboardingIntroViewModel) { + self.model = model + } + + var body: some View { + ZStack { + OnboardingBackground() + + switch model.state { + case .landing: + landingView + case let .onboarding(viewState): + onboardingDialogView(state: viewState) + } + } + } + + private func onboardingDialogView(state: ViewState.Intro) -> some View { + GeometryReader { geometry in + VStack(alignment: .center) { + DaxDialogView( + logoPosition: .top, + matchLogoAnimation: (Self.daxGeometryEffectID, animationNamespace), + showDialogBox: $showDaxDialogBox, + onTapGesture: { + withAnimation { + switch model.state { + case .onboarding(.startOnboardingDialog): + showIntroButton = true + animateIntroText = false + case .onboarding(.browsersComparisonDialog): + showComparisonButton = true + animateComparisonText = false + default: break + } + } + }, + content: { + VStack { + switch state { + case .startOnboardingDialog: + introView + case .browsersComparisonDialog: + browsersComparisonView + } + } + } + ) + } + .frame(width: geometry.size.width, alignment: .center) + .offset(y: geometry.size.height * Metrics.dialogVerticalOffsetPercentage.build(v: verticalSizeClass, h: horizontalSizeClass)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + Metrics.daxDialogVisibilityDelay) { + showDaxDialogBox = true + animateIntroText = true + } + } + } + .padding() + } + + private var landingView: some View { + return LandingView(animationNamespace: animationNamespace) + .ignoresSafeArea(edges: .bottom) + .frame(maxHeight: .infinity, alignment: .bottom) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + Metrics.daxDialogDelay) { + withAnimation { + model.onAppear() + } + } + } + } + + private var introView: some View { + IntroDialogContent(animateText: $animateIntroText, showCTA: $showIntroButton) { + animateBrowserComparisonViewState() + } + .onboardingDaxDialogStyle() + .visibility(showIntroViewContent ? .visible : .invisible) + } + + private var browsersComparisonView: some View { + BrowsersComparisonContent( + animateText: $animateComparisonText, + showContent: $showComparisonButton, + setAsDefaultBrowserAction: { + model.setDefaultBrowserAction() + }, cancelAction: { + model.cancelSetDefaultBrowserAction() + } + ) + .onboardingDaxDialogStyle() + } + + private func animateBrowserComparisonViewState() { + // Hide content of Intro dialog before animating + showIntroViewContent = false + + // Animation with small delay for a better effect when intro content disappear + let animationDuration = Metrics.comparisonChartAnimationDuration + let animation = Animation + .linear(duration: animationDuration) + .delay(0.2) + + if #available(iOS 17, *) { + withAnimation(animation) { + model.startOnboardingAction() + } completion: { + animateComparisonText = true + } + } else { + withAnimation(animation) { + model.startOnboardingAction() + } + DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { + animateComparisonText = true + } + } + } +} + +// MARK: - View State + +extension OnboardingView { + + enum ViewState: Equatable { + case landing + case onboarding(Intro) + } + +} + +extension OnboardingView.ViewState { + + enum Intro: Equatable { + case startOnboardingDialog + case browsersComparisonDialog + } + +} + +// MARK: - Metrics + +private enum Metrics { + static let daxDialogDelay: TimeInterval = 2.0 + static let daxDialogVisibilityDelay: TimeInterval = 0.5 + static let comparisonChartAnimationDuration = 0.25 + static let dialogVerticalOffsetPercentage = MetricBuilder(value: 0.1).smallIphone(0.01) +} + +// MARK: - Preview + +#Preview("Onboarding - Light") { + OnboardingView(model: .init()) + .preferredColorScheme(.light) +} + +#Preview("Onboarding - Dark") { + OnboardingView(model: .init()) + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift b/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift new file mode 100644 index 0000000000..d1310a5627 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift @@ -0,0 +1,90 @@ +// +// OnboardingPixelReporter.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core + +// MARK: - Pixel Fire Interface + +protocol OnboardingPixelFiring { + static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) +} + +extension Pixel: OnboardingPixelFiring { + static func fire(pixel: Event, withAdditionalParameters params: [String: String], includedParameters: [QueryParameters]) { + self.fire(pixel: pixel, withAdditionalParameters: params, includedParameters: includedParameters, onComplete: { _ in }) + } +} + +extension UniquePixel: OnboardingPixelFiring { + static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) { + self.fire(pixel: pixel, withAdditionalParameters: params, includedParameters: includedParameters, onComplete: { _ in }) + } +} + +// MARK: - OnboardingPixelReporter + +protocol OnboardingIntroImpressionReporting { + func trackOnboardingIntroImpression() +} + +protocol OnboardingIntroPixelReporting: OnboardingIntroImpressionReporting { + func trackBrowserComparisonImpression() + func trackChooseBrowserCTAAction() +} + +// MARK: - Implementation + +final class OnboardingPixelReporter { + private let pixel: OnboardingPixelFiring.Type + private let uniquePixel: OnboardingPixelFiring.Type + + init(pixel: OnboardingPixelFiring.Type = Pixel.self, uniquePixel: OnboardingPixelFiring.Type = UniquePixel.self) { + self.pixel = pixel + self.uniquePixel = uniquePixel + } + + private func fire(event: Pixel.Event, unique: Bool, additionalParameters: [String: String] = [:]) { + let parameters: [Pixel.QueryParameters] = [.appVersion, .atb] + if unique { + uniquePixel.fire(pixel: event, withAdditionalParameters: additionalParameters, includedParameters: parameters) + } else { + pixel.fire(pixel: event, withAdditionalParameters: additionalParameters, includedParameters: parameters) + } + } + +} + +// MARK: - OnboardingAnalytics + Intro + +extension OnboardingPixelReporter: OnboardingIntroPixelReporting { + + func trackOnboardingIntroImpression() { + fire(event: .onboardingIntroShownUnique, unique: true) + } + + func trackBrowserComparisonImpression() { + fire(event: .onboardingIntroComparisonChartShownUnique, unique: true) + } + + func trackChooseBrowserCTAAction() { + fire(event: .onboardingIntroChooseBrowserCTAPressed, unique: false) + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift new file mode 100644 index 0000000000..147955a73a --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift @@ -0,0 +1,46 @@ +// +// DaxDialogStyles.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +extension OnboardingStyles { + + struct DaxDialogStyle: ViewModifier { + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + func body(content: Content) -> some View { + content + .frame(maxWidth: Metrics.daxDialogMaxWidth.build(v: verticalSizeClass, h: horizontalSizeClass)) + } + + } + +} + +private enum Metrics { + static let daxDialogMaxWidth = MetricBuilder(iPhone: nil, iPad: 480) +} + +extension View { + + func onboardingDaxDialogStyle() -> some View { + modifier(OnboardingStyles.DaxDialogStyle()) + } +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift new file mode 100644 index 0000000000..683ae77757 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift @@ -0,0 +1,52 @@ +// +// OnboardingTextStyles.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +enum OnboardingStyles {} + +extension OnboardingStyles { + + struct TitleStyle: ViewModifier { + let fontSize: CGFloat + + func body(content: Content) -> some View { + let view = content + .font(.system(size: fontSize, weight: .bold)) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + + if #available(iOS 16, *) { + return view.kerning(0.38) + } else { + return view + } + } + + } + +} + +extension View { + + func onboardingTitleStyle(fontSize: CGFloat) -> some View { + modifier(OnboardingStyles.TitleStyle(fontSize: fontSize)) + } + +} diff --git a/DuckDuckGo/RootDebugViewController+Onboarding.swift b/DuckDuckGo/RootDebugViewController+Onboarding.swift new file mode 100644 index 0000000000..c25df3aa50 --- /dev/null +++ b/DuckDuckGo/RootDebugViewController+Onboarding.swift @@ -0,0 +1,39 @@ +// +// RootDebugViewController+Onboarding.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +extension RootDebugViewController { + + func showOnboardingIntro() { + let controller = OnboardingIntroViewController() + controller.delegate = self + controller.modalPresentationStyle = .overFullScreen + present(controller: controller, fromView: self.view) + } + +} + +extension RootDebugViewController: OnboardingDelegate { + + func onboardingCompleted(controller: UIViewController) { + controller.presentingViewController?.dismiss(animated: true) + } + +} diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index ffa87d2894..66216547db 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -45,6 +45,7 @@ class RootDebugViewController: UITableViewController { case resetSendCrashLogs = 671 case refreshConfig = 672 case newTabPageSections = 674 + case showNewOnboardingIntro = 676 } @IBOutlet weak var shareButton: UIBarButtonItem! @@ -167,6 +168,8 @@ class RootDebugViewController: UITableViewController { case .newTabPageSections: let controller = UIHostingController(rootView: NewTabPageSectionsDebugView()) show(controller, sender: nil) + case .showNewOnboardingIntro: + showOnboardingIntro() } } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 45fae6038b..0464d99144 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1180,4 +1180,29 @@ But if you *do* want a peek under the hood, you can find more information about public static let homeTabShortcutAIChat = NSLocalizedString("home.tab.shortcut.ai.chat", value: "AI Chat", comment: "Shortcut title leading to AI Chat") public static let homeTabShortcutVPN = NSLocalizedString("home.tab.shortcut.vpn", value: "VPN", comment: "Shortcut title leading to VPN") public static let homeTabShortcutPasswords = NSLocalizedString("home.tab.shortcut.passwords", value: "Passwords", comment: "Shortcut title leading to Passwords") + + // MARK: - Dax Onboarding Experiment + public enum DaxOnboardingExperiment { + enum Intro { + public static let title = NSLocalizedString("onboarding.intro.title", value: "Hi there.\n\nReady for a better, more private internet?", comment: "The title of the onboarding dialog popup") + public static let cta = NSLocalizedString("onboarding.intro.cta", value: "Let’s do it!", comment: "Button to continue the onboarding process") + } + + enum BrowsersComparison { + public static let title = NSLocalizedString("onboarding.browsers.title", value: "Privacy protections activated!", comment: "The title of the dialog to show the privacy features that DuckDuckGo offers") + public static let cta = NSLocalizedString("onboarding.browsers.cta", value: "Choose Your Browser", comment: "Button to change the default browser") + + enum Features { + public static let privateSearch = NSLocalizedString("onboarding.browsers.features.privateSearch.title", value: "Search privately by default", comment: "Message to highlight browser capability of private searches") + public static let trackerBlockers = NSLocalizedString("onboarding.browsers.features.trackerBlocker.title", value: "Block 3rd-party trackers", comment: "Message to highlight browser capability ofblocking 3rd party trackers") + public static let cookiePopups = NSLocalizedString("onboarding.browsers.features.cookiePopups.title", value: "Block cookie pop-ups", comment: "Message to highlight browser capability of blocking cookie pop-ups") + public static let creepyAds = NSLocalizedString("onboarding.browsers.features.creepyAds.title", value: "Block creepy ads", comment: "Message to highlight browser capability of blocking creepy ads") + public static let eraseBrowsingData = NSLocalizedString("onboarding.browsers.features.eraseBrowsingData.title", value: "Swiftly erase browsing data", comment: "Message to highlight browser capability ofswiftly erase browsing data") + } + } + + enum DefaultBrowser { + public static let message = NSLocalizedString("onboarding.defaultBrowser.message", value: "Open links with peace of mind, every time.", comment: "Subheader message for the screen to choose DuckDuckGo as default browser") + } + } } diff --git a/DuckDuckGo/ViewVisibility.swift b/DuckDuckGo/ViewVisibility.swift new file mode 100644 index 0000000000..3a33fc8cc3 --- /dev/null +++ b/DuckDuckGo/ViewVisibility.swift @@ -0,0 +1,44 @@ +// +// ViewVisibility.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// https://swiftuirecipes.com/blog/how-to-hide-a-swiftui-view-visible-invisible-gone +enum ViewVisibility: CaseIterable { + + case visible, // view is fully visible + invisible, // view is hidden but takes up space + gone // view is fully removed from the view hierarchy + +} + +extension View { + + // https://swiftuirecipes.com/blog/how-to-hide-a-swiftui-view-visible-invisible-gone + @ViewBuilder func visibility(_ visibility: ViewVisibility) -> some View { + if visibility != .gone { + if visibility == .visible { + self + } else { + hidden() + } + } + } + +} diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index c997501945..01bc7f53bf 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Изскачащият прозорец е скрит"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Изберете своя браузър"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Блокиране на изскачащи прозорци за бисквитки"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Блокиране на досадни реклами"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Бързо изтриване на данните за сърфиране"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Поверително търсене по подразбиране"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Блокиране на тракерите на трети страни"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Защитата на поверителността е активирана!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Отваряйте връзки без притеснения, всеки път."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Да го направим!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Здравейте.\n\nГотови ли сте за по-добро и по-поверително изживяване в интернет?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Добави приспособлението"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index d916ba23c1..331b559400 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Skryté vyskakovací okno"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Vyber si prohlížeč"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokování vyskakovacích oken ohledně cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokování reklam, které tě všude pronásledují"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Rychle vymaž údaje o procházení"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Soukromé vyhledávání ve výchozím nastavení"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokuj trackery třetích stran"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Ochrana osobních údajů je aktivní!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Otevírejte odkazy s klidem mysli, pokaždé."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Pojďme na to!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Zdravíme tě.\n\nTěšíš se na lepší internet s větší ochranou soukromí?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Přidat widget"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index c0f12afeb1..bfede1771e 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop op-vindue skjult"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Vælg din browser"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Bloker pop op-vinduer om cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Bloker uhyggelige annoncer"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Ryd hurtigt browserdata"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Søg fortroligt som standard"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Bloker tredjeparts-trackere"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Beskyttelse af privatlivet er aktiveret!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Åbne link med ro i sindet, hver gang."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Lad os gøre det!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hej\n\nEr du klar til et bedre og mere privat internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Tilføj widget"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 1ffb4a9fbc..029ca7cac1 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up ausgeblendet"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Wähle deinen Browser"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Cookie-Pop-ups blockieren"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Aufdringliche Werbung blockieren"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Browserdaten schnell löschen"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Standardmäßig privat suchen"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blockiert Tracker von Drittanbietern"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Datenschutz aktiviert!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Öffne Links jederzeit sicher und ohne Sorge."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Los geht's!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hallo.\n\nBereit für ein besseres, privateres Internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Widget hinzufügen"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 8180e33a74..e7cb7fe4d1 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Κρυφό αναδυόμενο παράθυρο"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Επιλέξτε το πρόγραμμα περιήγησής σας"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Αποκλεισμός αναδυόμενων παραθύρων cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Αποκλεισμός στοχευμένων διαφημίσεων"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Γρήγορη διαγραφή δεδομένων περιήγησης"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Ιδιωτική αναζήτηση βάσει προεπιλογής"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Αποκλείστε εφαρμογές παρακολούθησης τρίτων"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "H προστασία προσωπικών δεδομένων energopoi;huhke!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Ανοίξτε συνδέσμους με ηρεμία και ασφάλεια, κάθε φορά."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Ας το δοκιμάσουμε!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Γεια σας.\n\nΈτοιμοι για ένα καλύτερο και πιο ιδιωτικό διαδίκτυο;"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Προσθήκη γραφικού στοιχείου"; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index de9933f9a0..e28055ff28 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1585,6 +1585,36 @@ https://duckduckgo.com/mac"; /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up Hidden"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Choose Your Browser"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Block cookie pop-ups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Block creepy ads"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Swiftly erase browsing data"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Search privately by default"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Block 3rd-party trackers"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Privacy protections activated!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Open links with peace of mind, every time."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Let’s do it!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hi there.\n\nReady for a better, more private internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Add Widget"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 0d187667e7..08ea645776 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Ventana emergente oculta"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Elige tu navegador"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Bloqueo de ventanas emergentes de cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Bloqueo de anuncios escalofriantes"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Borra rápidamente los datos de navegación"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Búsqueda privada por defecto"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Bloquea rastreadores de terceros"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "¡Protecciones de privacidad activadas!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Abre los enlaces con tranquilidad, siempre."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "¡Adelante!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hola.\n\n¿Listo para un internet mejor y más privado?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Añadir widget"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index d969f50d56..6fd74079b3 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Hüpikaken on peidetud"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Vali oma brauser"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokeeri küpsiste hüpikaknad"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokeeri sihitud reklaamid"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Sirvimisandmete kiire kustutamine"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Otsi vaikimisi privaatselt"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokeeri kolmanda poole jälgurid"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Privaatsuskaitsed on aktiveeritud!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Ava linke igal ajal, rahuliku meelega."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Teeme ära!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Tere!\n\nKas oled valmis kasutama paremat ja privaatsemat internetti?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Lisa vidin"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 89b9181452..0cf8404a2d 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Ponnahdusikkuna piilotettu"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Valitse selaimesi"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Estä evästeponnahdusikkunat"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Estä rasittavat mainokset"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Tyhjennä selaustiedot nopeasti"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Yksityinen selaus oletuksena"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Estä kolmannen osapuolen seuraimet"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Yksityisyyden suoja aktivoitu!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Avaa linkit ilman stressiä, joka kerta."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Aloitetaan!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hei!\n\nOletko valmis parempaan ja yksityisempään internetiin?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Lisää widget"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index 4a7880583f..ce4ef334e7 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Fenêtre contextuelle masquée"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Choisissez votre navigateur"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Bloquez les fenêtres contextuelles de cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Bloquez les publicités douteuses"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Effacer rapidement les données de navigation"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Faire une recherche privée par défaut"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Bloquer les traqueurs tiers"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Protections de la confidentialité activées !"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Des liens ouverts en toute tranquillité d'esprit, à chaque fois."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "C'est parti !"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Bonjour.\n\nEnvie de profiter d'Internet d'une meilleure façon plus confidentielle ?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Ajouter un widget"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 83e98d3806..028ccf6bbb 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Skočni prozor je sakriven"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Odaberi preglednik"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokiraj skočne prozore kolačića"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokiraj neželjene oglase"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Brzo izbriši podatke pregledavanja"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Pretražuj privatno po zadanim postavkama"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokiraj alate za praćenje trećih strana"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Zaštita privatnosti aktivirana!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Svaki put poveznice otvori spokojno."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Učinimo to!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Bok!\n\nJesi li spreman za bolji, privatniji internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Dodaj widget"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index eb4d09ba8a..0dd424f998 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Felugró ablak elrejtve"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Böngésző kiválasztása"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Felugró sütiablakok blokkolása"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Gyanús hirdetések blokkolása"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Böngészési adatok gyors törlése"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Privát keresés alapértelmezés szerint"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Harmadik féltől származó nyomkövetők blokkolása"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Adatvédelem aktiválva!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Nyugodtan kattints a linkekre, mindig."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Rajta, csináljuk!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Szia!\n\nKészen állsz egy jobb, privátabb internetre?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Minialkalmazás hozzáadása"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index 21b65d3c4d..5a3aadfb2c 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Popup nascosto"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Scegli il tuo browser"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blocca i popup dei cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blocca gli annunci invasivi"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Elimina rapidamente i dati di navigazione"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Cerca privatamente per impostazione predefinita"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blocca i sistemi di tracciamento di terze parti"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Protezioni della privacy attivate!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Apri i collegamenti con la massima tranquillità, ogni volta."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Continua"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Ciao.\n\nVuoi navigare in Internet con più privacy?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Aggiungi widget"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index 42feaed1c0..456e979471 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Iškylantysis langas paslėptas"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Pasirinkite naršyklę"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokuoti slapukų iššokančiuosius langus"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokuokite taikomus skelbimus"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Greitai ištrinkite naršymo duomenis"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Vykdykite paiešką privačiai pagal numatytąjį nustatymą"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokuoja trečiųjų šalių stebėjimo priemones"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Privatumo apsaugos priemonės įjungtos!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Kiekvieną kartą be jaudulio atverkite nuorodas."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Pirmyn!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Sveiki.\n\nAr esate pasiruošę geresniam ir privatesniam internetui?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Pridėti valdiklį"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index fa318dbbba..60ee5957da 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Uznirstošais logs paslēpts"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Izvēlies pārlūku"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Bloķē sīkfailu uznirstošos logus"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Bloķē kaitinošas reklāmas"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Ātri izdzēs pārlūkošanas datus"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Meklē privāti pēc noklusējuma"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Bloķē trešo pušu izsekotājus"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Privātuma aizsardzība ir aktivizēta!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Katru reizi atver saites bez raizēm."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Aiziet!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Sveiki!\n\nVai esi gatavs labākam, privātākam Internetam?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Pievienot logrīku"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index c12d524984..4b15b6c41a 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Popup-vindu skjult"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Velg nettleseren din"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokker popup-vinduer om informasjonskapsler"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokker påtrengende annonser"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Slett nettleserdata raskt"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Søk privat som standard"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokker tredjepartssporere"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Personvern er aktivert!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Åpne lenker med ro i sinnet, hver gang."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Sett i gang!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Heisann.\n\nKlar for et bedre, mer privat internett?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Legg til widget"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index 616ae54984..a2cb6271dc 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up verborgen"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Kies je browser"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokkeer cookiepop-ups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokkeer enge advertenties"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Wis browsegegevens in een handomdraai"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Standaard privé zoeken"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokkeer trackers van derden"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Privacybescherming geactiveerd!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Open links altijd met een gerust hart."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Laten we het doen!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hoi.\n\nKlaar voor een beter internet met meer privacy?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Widget toevoegen"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 02689aa9e2..be4679c130 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Ukryte wyskakujące okienka"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Wybierz przeglądarkę"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokuj wyskakujące okienka z informacją o plikach cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokuj wścibskie reklamy"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Szybko usuwaj dane przeglądania"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Domyślnie szukaj prywatnie"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokuj mechanizmy śledzące innych firm"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Aktywowano ochronę prywatności!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Zawsze otwieraj linki ze spokojem."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Zróbmy to!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Cześć!\n\nChcesz korzystać z lepszego, bardziej prywatnego Internetu?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Dodaj widżet"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index e4db35ec48..612854e580 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up ocultado"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Escolhe o teu navegador"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Bloquear pop-ups de cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Bloquear anúncios assustadores"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Apaga rapidamente os dados de navegação"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Pesquisa em privado por predefinição"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Bloqueia rastreadores de terceiros"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Proteções de privacidade ativadas!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Abra links com tranquilidade, sempre."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Vamos lá!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Olá!\n\nA postos para uma Internet melhor e mais privada?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Adicionar widget"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index d7e7c6f3dc..7b4c25283c 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up ascuns"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Alege browserul"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blochează ferestrele pop-up privind modulele cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blochează reclamele înfiorătoare"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Șterge rapid datele de navigare"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Caută implicit în mod privat"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blochează tehnologiile de urmărire terțe"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Măsurile de protecție a confidențialității au fost activate!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Deschide link-urile liniștit, de fiecare dată."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Să începem!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Salut!\n\nEști gata pentru un internet mai bun și mai privat?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Adăugare widget"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 19dc60242f..549677b12d 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Всплывающее окно скрыто"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Выбрать браузер"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Блокировка всплывающих окон куки"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Защита от надоедливой рекламы"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Быстрая очистка данных из браузера"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Конфиденциальный поиск по умолчанию"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Блокировка сторонних трекеров"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Защита конфиденциальности уже включена!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Открывайте ссылки, ни о чем не беспокоясь."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Поехали!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Привет!\n\nКачественный интернет с защитой данных заказывали?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Добавить виджет"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 277ba1b449..698a7e7f2a 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Kontextové okno je skryté"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Vyberte si prehliadač"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokovanie vyskakovacích okien o súboroch cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokovať nepríjemné reklamy"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Rýchle vymazanie údajov prehliadania"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Predvolené vyhľadávanie so súkromím"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokovať sledovanie tretími stranami"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Ochrana súkromia bola aktivovaná!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Vždy otvor odkazy s pokojom mysle."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Poďme na to!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Dobrý deň.\n\nSte pripravený/-á na lepší, súkromnejší internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Pridať miniaplikáciu"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 76f00ef372..9aea6c5ba0 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pojavno okno je skrito"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Izberite brskalnik"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blokirajte pojavna okna za piškotke"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blokirajte srhljive oglase"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Hitro izbrišite podatke brskanja"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Privzeto išče zasebno"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blokirajte sledilnike tretjih oseb"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Zaščite zasebnosti so aktivirane!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Bodite pomirjeni, ko odpirate povezave."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Pa začnimo!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Pozdravljeni.\n\nSte pripravljeni na boljši, bolj zasebni internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Dodaj gradnik"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index 4eef979147..e6135b1664 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Popup-fönster dolt"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Välj din webbläsare"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Blockera popup-fönster för cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Blockera påträngande annonser"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Rensa snabbt surfdata"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Sök privat som standard"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Blockera spårare från tredje part"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Integritetsskydd aktiverat!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Sluta oroa dig varje gång du öppnar länkar."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Vi kör!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Hej!\n\nÄr du redo för ett bättre, mer privat internet?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Lägg till widget"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index 90c99493a2..0cd2918f0a 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -1633,6 +1633,36 @@ /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Açılır Pencere Gizli"; +/* Button to change the default browser */ +"onboarding.browsers.cta" = "Tarayıcınızı Seçin"; + +/* Message to highlight browser capability of blocking cookie pop-ups */ +"onboarding.browsers.features.cookiePopups.title" = "Çerez açılır pencerelerini engelle"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.browsers.features.creepyAds.title" = "Tekinsiz reklamları engelleyin"; + +/* Message to highlight browser capability ofswiftly erase browsing data */ +"onboarding.browsers.features.eraseBrowsingData.title" = "Tarama verilerini hızlıca silin"; + +/* Message to highlight browser capability of private searches */ +"onboarding.browsers.features.privateSearch.title" = "Varsayılan olarak gizli arama yapın"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.browsers.features.trackerBlocker.title" = "Üçüncü taraf izleyicileri engelleyin"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.browsers.title" = "Gizlilik korumaları etkinleştirildi!"; + +/* Subheader message for the screen to choose DuckDuckGo as default browser */ +"onboarding.defaultBrowser.message" = "Her zaman gönül rahatlığıyla açık bağlantılar."; + +/* Button to continue the onboarding process */ +"onboarding.intro.cta" = "Hadi Başlayalım!"; + +/* The title of the onboarding dialog popup */ +"onboarding.intro.title" = "Merhaba.\n\nDaha iyi, daha özel bir internete hazır mısınız?"; + /* No comment provided by engineer. */ "onboarding.widgets.continueButton" = "Widget Ekle"; diff --git a/DuckDuckGoTests/AnimatableTypingTextModelTests.swift b/DuckDuckGoTests/AnimatableTypingTextModelTests.swift new file mode 100644 index 0000000000..dffcef4898 --- /dev/null +++ b/DuckDuckGoTests/AnimatableTypingTextModelTests.swift @@ -0,0 +1,188 @@ +// +// AnimatableTypingTextModelTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import DuckDuckGo + +final class AnimatableTypingTextModelTests: XCTestCase { + private var factoryMock: MockTimerFactory! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + factoryMock = MockTimerFactory() + cancellables = [] + } + + override func tearDownWithError() throws { + factoryMock = nil + cancellables = nil + try super.tearDownWithError() + } + + func testWhenStartAnimatingIsCalledThenTimerIsStarted() { + // GIVEN + let sut = AnimatableTypingTextModel(text: "Hello World!!!", onTypingFinished: nil, timerFactory: factoryMock) + XCTAssertFalse(factoryMock.didCallMakeTimer) + XCTAssertNil(factoryMock.capturedInterval) + XCTAssertNil(factoryMock.capturedRepeats) + + // WHEN + sut.startAnimating() + + // THEN + XCTAssertTrue(factoryMock.didCallMakeTimer) + XCTAssertEqual(factoryMock.capturedInterval, 0.02) + XCTAssertEqual(factoryMock.capturedRepeats, true) + } + + func testWhenStopAnimatingIsCalledThenTimerIsInvalidate() throws { + // GIVEN + let sut = AnimatableTypingTextModel(text: "Hello World!!!", onTypingFinished: nil, timerFactory: factoryMock) + sut.startAnimating() + let timerMock = try XCTUnwrap(factoryMock.createdTimer) + XCTAssertFalse(timerMock.didCallInvalidate) + + // WHEN + sut.stopAnimating() + + // THEN + XCTAssertTrue(timerMock.didCallInvalidate) + } + + func testWhenTimerFiresThenTypedTextIsPublished_iOS15() throws { + guard #available(iOS 15, *) else { + throw XCTSkip("Test available only on iOS 15*") + } + + // GIVEN + let text = "Hello World!!!" + var typedText: NSAttributedString = .init(string: "") + let sut = AnimatableTypingTextModel(text: text, onTypingFinished: nil, timerFactory: factoryMock) + sut.startAnimating() + let timerMock = try XCTUnwrap(factoryMock.createdTimer) + sut.$typedAttributedText + .dropFirst() + .sink { attributedString in + typedText = attributedString + } + .store(in: &cancellables) + XCTAssertTrue(typedText.string.isEmpty) + + for i in 0 ..< text.count { + // WHEN + timerMock.fire() + + // THEN checks that the right character doesn't have clear color applied but the rest of the string has + XCTAssertTrue(assertTypedChar(forTypedText: typedText, at: i)) + } + } + + func testWhenStopAnimatingIsCalledThenWholeTextIsPublished_iOS15() throws { + guard #available(iOS 15, *) else { + throw XCTSkip("Test available only on iOS 15*") + } + + // GIVEN + let text = "Hello World!!!" + var typedText: NSAttributedString = .init(string: "") + let sut = AnimatableTypingTextModel(text: text, onTypingFinished: nil, timerFactory: factoryMock) + sut.startAnimating() + let timerMock = try XCTUnwrap(factoryMock.createdTimer) + sut.$typedAttributedText + .dropFirst() + .sink { attributedString in + typedText = attributedString + } + .store(in: &cancellables) + XCTAssertTrue(typedText.string.isEmpty) + timerMock.fire() + + // WHEN + sut.stopAnimating() + + // THEN the string does not have any clear character + XCTAssertEqual(typedText.string, text) + let attributes = typedText.attributes(at: 0, effectiveRange: nil) + let foregroundcColor = attributes[.foregroundColor] as? UIColor + XCTAssertNil(foregroundcColor) + } + + func testWhenTimerFiresLastCharOfTextThenTimerIsInvalidated() throws { + // GIVEN + let text = "Hello World!!!" + let sut = AnimatableTypingTextModel(text: text, onTypingFinished: nil, timerFactory: factoryMock) + sut.startAnimating() + let timerMock = try XCTUnwrap(factoryMock.createdTimer) + XCTAssertFalse(timerMock.didCallInvalidate) + + // WHEN + text.forEach { _ in + timerMock.fire() + } + timerMock.fire() // Simulate timer firing after whole text shown + + // THEN + XCTAssertTrue(timerMock.didCallInvalidate) + } + + func testWhenTimerFinishesThenOnTypingFinishedBlockIsCalled() throws { + // GIVEN + let expectation = self.expectation(description: #function) + let text = "Hello World!!!" + let sut = AnimatableTypingTextModel(text: text, onTypingFinished: { expectation.fulfill() }, timerFactory: factoryMock) + sut.startAnimating() + let timerMock = try XCTUnwrap(factoryMock.createdTimer) + + // WHEN + text.forEach { _ in + timerMock.fire() + } + timerMock.fire() // Simulate timer firing after whole text shown + + // THEN + waitForExpectations(timeout: 2.0) + } + +} + +private extension AnimatableTypingTextModelTests { + + func assertTypedChar(forTypedText typedText: NSAttributedString, at position: Int) -> Bool { + let typedTextAttribute = typedText.attribute(.foregroundColor, at: position, effectiveRange: nil) + + let location = position + 1 + + // If it's the last char just check the currenct character as there's no remaining string to check + guard location < typedText.length else { + return typedTextAttribute == nil + } + + // Checks that the remaining substring has a clear color + let remainingTextRange = NSRange(location: location, length: typedText.string.count) + let remainingTextAttributes = typedText.attributes(at: location, longestEffectiveRange: nil, in: remainingTextRange) + let remainingTextForegroundColor = remainingTextAttributes[.foregroundColor] as? UIColor + + return typedTextAttribute == nil && + remainingTextForegroundColor == .clear + } + +} diff --git a/DuckDuckGoTests/MockTimer.swift b/DuckDuckGoTests/MockTimer.swift new file mode 100644 index 0000000000..22ce47b43d --- /dev/null +++ b/DuckDuckGoTests/MockTimer.swift @@ -0,0 +1,63 @@ +// +// MockTimer.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import Core + +final class MockTimer: TimerInterface { + var isValid: Bool = true + private(set) var didCallInvalidate = false + + let timeInterval: TimeInterval + let repeats: Bool + private let block: (TimerInterface) -> Void + + init(timeInterval: TimeInterval, repeats: Bool, block: @escaping (TimerInterface) -> Void) { + self.timeInterval = timeInterval + self.repeats = repeats + self.block = block + } + + func invalidate() { + didCallInvalidate = true + } + + func fire() { + block(self) + } +} + +final class MockTimerFactory: TimerCreating { + private(set) var didCallMakeTimer = false + private(set) var capturedInterval: TimeInterval? + private(set) var capturedRepeats: Bool? + + private(set) var createdTimer: MockTimer? + + func makeTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (TimerInterface) -> Void) -> TimerInterface { + didCallMakeTimer = true + capturedInterval = interval + capturedRepeats = repeats + + let timer = MockTimer(timeInterval: interval, repeats: repeats, block: block) + createdTimer = timer + return timer + } + +} diff --git a/DuckDuckGoTests/MockURLOpener.swift b/DuckDuckGoTests/MockURLOpener.swift new file mode 100644 index 0000000000..79a5b9fe6e --- /dev/null +++ b/DuckDuckGoTests/MockURLOpener.swift @@ -0,0 +1,41 @@ +// +// MockURLOpener.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import Core + +final class MockURLOpener: URLOpener { + private(set) var didCallCanOpenURL = false + private(set) var didCallOpenURL = false + private(set) var capturedURL: URL? + + var canOpenURL = false + + func canOpenURL(_ url: URL) -> Bool { + didCallCanOpenURL = true + capturedURL = url + return canOpenURL + } + + func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { + didCallOpenURL = true + capturedURL = url + } + +} diff --git a/DuckDuckGoTests/OnboardingFirePixelMock.swift b/DuckDuckGoTests/OnboardingFirePixelMock.swift new file mode 100644 index 0000000000..1c790f3f82 --- /dev/null +++ b/DuckDuckGoTests/OnboardingFirePixelMock.swift @@ -0,0 +1,64 @@ +// +// OnboardingFirePixelMock.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core +@testable import DuckDuckGo + +final class OnboardingPixelFireMock: OnboardingPixelFiring { + static private(set) var didCallFire = false + static private(set) var capturedPixelEvent: Pixel.Event? + static private(set) var capturedParams: [String: String] = [:] + static private(set) var capturedIncludeParameters: [Pixel.QueryParameters] = [] + + static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) { + didCallFire = true + capturedPixelEvent = pixel + capturedParams = params + capturedIncludeParameters = includedParameters + } + + static func tearDown() { + didCallFire = false + capturedPixelEvent = nil + capturedParams = [:] + capturedIncludeParameters = [] + } +} + +final class OnboardingUniquePixelFireMock: OnboardingPixelFiring { + static private(set) var didCallFire = false + static private(set) var capturedPixelEvent: Pixel.Event? + static private(set) var capturedParams: [String: String] = [:] + static private(set) var capturedIncludeParameters: [Pixel.QueryParameters] = [] + + static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) { + didCallFire = true + capturedPixelEvent = pixel + capturedParams = params + capturedIncludeParameters = includedParameters + } + + static func tearDown() { + didCallFire = false + capturedPixelEvent = nil + capturedParams = [:] + capturedIncludeParameters = [] + } +} diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift new file mode 100644 index 0000000000..52342a9bb4 --- /dev/null +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -0,0 +1,168 @@ +// +// OnboardingIntroViewModelTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo + +final class OnboardingIntroViewModelTests: XCTestCase { + + // MARK: - State + Actions + + func testWhenSubscribeToViewStateThenShouldSendLanding() { + // GIVEN + let sut = OnboardingIntroViewModel(urlOpener: MockURLOpener()) + + // WHEN + let result = sut.state + + // THEN + XCTAssertEqual(result, .landing) + } + + func testWhenOnAppearIsCalledThenViewStateChangesToStartOnboardingDialog() { + // GIVEN + let sut = OnboardingIntroViewModel(urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.startOnboardingDialog)) + } + + func testWhenStartOnboardingActionIsCalledThenViewStateChangesToBrowsersComparisonDialog() { + // GIVEN + let sut = OnboardingIntroViewModel() + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.browsersComparisonDialog)) + } + + func testWhenSetDefaultBrowserActionIsCalledThenURLOpenerOpensSettingsURL() { + // GIVEN + let urlOpenerMock = MockURLOpener() + let sut = OnboardingIntroViewModel(urlOpener: urlOpenerMock) + XCTAssertFalse(urlOpenerMock.didCallOpenURL) + XCTAssertNil(urlOpenerMock.capturedURL) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(urlOpenerMock.didCallOpenURL) + XCTAssertEqual(urlOpenerMock.capturedURL?.absoluteString, UIApplication.openSettingsURLString) + } + + func testWhenSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + + func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.cancelSetDefaultBrowserAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + + // MARK: - Pixels + + func testWhenOnAppearIsCalledThenPixelReporterTrackOnboardingIntroImpression() { + // GIVEN + let pixelReporterMock = OnboardingIntroPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackOnboardingIntroImpression) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackOnboardingIntroImpression) + } + + func testWhenStartOnboardingActionIsCalledThenPixelReporterTrackBrowserComparisonImpression() { + // GIVEN + let pixelReporterMock = OnboardingIntroPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackBrowserComparisonImpression) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackBrowserComparisonImpression) + } + + func testWhenChooseBrowserIsCalledThenPixelReporterTrackChooseBrowserCTAAction() { + // GIVEN + let pixelReporterMock = OnboardingIntroPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackChooseBrowserCTAAction) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackChooseBrowserCTAAction) + } + +} + +private final class OnboardingIntroPixelReporterMock: OnboardingIntroPixelReporting { + private(set) var didCallTrackOnboardingIntroImpression = false + private(set) var didCallTrackBrowserComparisonImpression = false + private(set) var didCallTrackChooseBrowserCTAAction = false + + func trackOnboardingIntroImpression() { + didCallTrackOnboardingIntroImpression = true + } + + func trackBrowserComparisonImpression() { + didCallTrackBrowserComparisonImpression = true + } + + func trackChooseBrowserCTAAction() { + didCallTrackChooseBrowserCTAAction = true + } +} diff --git a/DuckDuckGoTests/OnboardingPixelReporterTests.swift b/DuckDuckGoTests/OnboardingPixelReporterTests.swift new file mode 100644 index 0000000000..b89a4f18a1 --- /dev/null +++ b/DuckDuckGoTests/OnboardingPixelReporterTests.swift @@ -0,0 +1,96 @@ +// +// OnboardingPixelReporterTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Core +@testable import DuckDuckGo + +final class OnboardingPixelReporterTests: XCTestCase { + private var sut: OnboardingPixelReporter! + + override func setUpWithError() throws { + sut = OnboardingPixelReporter(pixel: OnboardingPixelFireMock.self, uniquePixel: OnboardingUniquePixelFireMock.self) + try super.setUpWithError() + } + + override func tearDownWithError() throws { + OnboardingPixelFireMock.tearDown() + OnboardingUniquePixelFireMock.tearDown() + sut = nil + try super.tearDownWithError() + } + + func testWhenTrackOnboardingIntroImpressionThenOnboardingIntroShownEventFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroShownUnique + XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackOnboardingIntroImpression() + + // THEN + XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, "m_preonboarding_intro_shown_unique") + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + + func testWhenTrackBrowserComparisonImpressionThenOnboardingIntroComparisonChartShownEventFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroComparisonChartShownUnique + XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackBrowserComparisonImpression() + + // THEN + XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, "m_preonboarding_comparison_chart_shown_unique") + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + + func testWhenTrackChooseBrowserCTAActionThenOnboardingIntroChooseBrowserCTAPressedEventFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroChooseBrowserCTAPressed + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackChooseBrowserCTAAction() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, "m_preonboarding_choose_browser_pressed") + XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + +} diff --git a/LocalPackages/DuckUI/Sources/DuckUI/Button.swift b/LocalPackages/DuckUI/Sources/DuckUI/Button.swift index f5513da230..ef7c90261c 100644 --- a/LocalPackages/DuckUI/Sources/DuckUI/Button.swift +++ b/LocalPackages/DuckUI/Sources/DuckUI/Button.swift @@ -47,7 +47,7 @@ public struct PrimaryButtonStyle: ButtonStyle { .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.center) .lineLimit(nil) - .font(Font(UIFont.boldAppFont(ofSize: compact ? Consts.fontSize - 1 : Consts.fontSize))) + .font(Font(UIFont.boldAppFont(ofSize: Consts.fontSize))) .foregroundColor(configuration.isPressed ? pressedForegroundColor : foregroundColor) .padding(.vertical) .padding(.horizontal, fullWidth ? nil : 24) @@ -85,7 +85,7 @@ public struct SecondaryButtonStyle: ButtonStyle { public func makeBody(configuration: Configuration) -> some View { compactPadding(view: configuration.label) - .font(Font(UIFont.boldAppFont(ofSize: compact ? Consts.fontSize - 1 : Consts.fontSize))) + .font(Font(UIFont.boldAppFont(ofSize: Consts.fontSize))) .foregroundColor(configuration.isPressed ? foregroundColor.opacity(Consts.pressedOpacity) : foregroundColor.opacity(1)) .padding() .frame(minWidth: 0, maxWidth: .infinity, maxHeight: compact ? Consts.height - 10 : Consts.height) @@ -120,7 +120,7 @@ public struct SecondaryFillButtonStyle: ButtonStyle { .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.center) .lineLimit(nil) - .font(Font(UIFont.boldAppFont(ofSize: compact ? Consts.fontSize - 1 : Consts.fontSize))) + .font(Font(UIFont.boldAppFont(ofSize: Consts.fontSize))) .foregroundColor(configuration.isPressed ? defaultForegroundColor : foregroundColor) .padding(.vertical) .padding(.horizontal, fullWidth ? nil : 24) @@ -174,6 +174,6 @@ public struct GhostButtonStyle: ButtonStyle { private enum Consts { static let cornerRadius: CGFloat = 8 static let height: CGFloat = 50 - static let fontSize: CGFloat = 16 + static let fontSize: CGFloat = 15 static let pressedOpacity: CGFloat = 0.7 } From 78d2593a48781543c28677c384ab106fe5d38a7b Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 15 Jul 2024 07:22:02 +0200 Subject: [PATCH 20/48] Add a debug menu action to reset Remote Messages on macOS (#3076) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207797025533577/f Description: This is a BSK bump for a macOS-related change. No effect on iOS. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 99564b7563..23a2eb6ebc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10118,7 +10118,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 171.0.0; + version = 171.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 06e50aefee..16c1672459 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "9ee9b378060b94aeafba65c62e629953fec91093", - "version" : "171.0.0" + "revision" : "c9462f5a01ef4b298caea661981fa6cecff687b7", + "version" : "171.1.0" } }, { From 1d9e40d564bfdd60832add512663d7e0d5c6b1dd Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Mon, 15 Jul 2024 12:01:25 +0100 Subject: [PATCH 21/48] bump apple toolbox --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/SyncUI/Package.swift | 2 +- LocalPackages/Waitlist/Package.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23a2eb6ebc..5f0fc9028a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10134,7 +10134,7 @@ repositoryURL = "https://github.com/duckduckgo/apple-toolbox.git"; requirement = { kind = exactVersion; - version = 3.1.1; + version = 3.1.2; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 16c1672459..1a241e12ad 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/apple-toolbox.git", "state" : { - "revision" : "ab53ca41e9044a20eab7e53249526fadcf9acc9f", - "version" : "3.1.1" + "revision" : "0c13c5f056805f2d403618ccc3bfb833c303c68d", + "version" : "3.1.2" } }, { diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 72196aeadb..d1da2db921 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -34,7 +34,7 @@ let package = Package( dependencies: [ .package(path: "../DuckUI"), .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.1"), + .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ .target( diff --git a/LocalPackages/Waitlist/Package.swift b/LocalPackages/Waitlist/Package.swift index 66c84034d6..59aa29a33f 100644 --- a/LocalPackages/Waitlist/Package.swift +++ b/LocalPackages/Waitlist/Package.swift @@ -16,7 +16,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.1"), + .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ .target( From 6a5250794ceead66aa05300af238dad4f8d2363d Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Mon, 15 Jul 2024 12:01:47 +0100 Subject: [PATCH 22/48] Revert "bump apple toolbox" This reverts commit 1d9e40d564bfdd60832add512663d7e0d5c6b1dd. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/SyncUI/Package.swift | 2 +- LocalPackages/Waitlist/Package.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5f0fc9028a..23a2eb6ebc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10134,7 +10134,7 @@ repositoryURL = "https://github.com/duckduckgo/apple-toolbox.git"; requirement = { kind = exactVersion; - version = 3.1.2; + version = 3.1.1; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1a241e12ad..16c1672459 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/apple-toolbox.git", "state" : { - "revision" : "0c13c5f056805f2d403618ccc3bfb833c303c68d", - "version" : "3.1.2" + "revision" : "ab53ca41e9044a20eab7e53249526fadcf9acc9f", + "version" : "3.1.1" } }, { diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index d1da2db921..72196aeadb 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -34,7 +34,7 @@ let package = Package( dependencies: [ .package(path: "../DuckUI"), .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), + .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.1"), ], targets: [ .target( diff --git a/LocalPackages/Waitlist/Package.swift b/LocalPackages/Waitlist/Package.swift index 59aa29a33f..66c84034d6 100644 --- a/LocalPackages/Waitlist/Package.swift +++ b/LocalPackages/Waitlist/Package.swift @@ -16,7 +16,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), + .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.1"), ], targets: [ .target( From 48f8175a974a708c4409584a7c3477291c31c296 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Mon, 15 Jul 2024 12:08:28 +0100 Subject: [PATCH 23/48] add fire button test (#3071) --- .maestro/release_tests/firebutton.yaml | 106 +++++++++++++++++++++++++ .maestro/run_ui_tests.sh | 1 - 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 .maestro/release_tests/firebutton.yaml diff --git a/.maestro/release_tests/firebutton.yaml b/.maestro/release_tests/firebutton.yaml new file mode 100644 index 0000000000..0cdff6e4a8 --- /dev/null +++ b/.maestro/release_tests/firebutton.yaml @@ -0,0 +1,106 @@ +# firebutton.yaml +appId: com.duckduckgo.mobile.ios +tags: + - release + +--- + +# Set up +- clearState +- clearKeychain +- launchApp + +- runFlow: + file: ../shared/onboarding.yaml + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter + +# Manage onboarding +- runFlow: + file: ../shared/onboarding_browsing.yaml + +# Add a cookie +- assertVisible: "Storage Counter: undefined" +- assertVisible: "Cookie Counter:" +- assertNotVisible: "Cookie Counter: 1" +- assertNotVisible: "Storage Counter: 1" +- assertVisible: "Manual Increment" +- tapOn: "Manual Increment" +- assertVisible: "Cookie Counter: 1" +- assertVisible: "Storage Counter: 1" + +# Load a new tab +- longPressOn: "Tab Switcher" +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://example.com" +- pressKey: Enter + +# Check history +- longPressOn: "Tab Switcher" + +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "ex" +- assertVisible: "example.com" +- assertVisible: "Example Domain" +- tapOn: "Cancel" + +# Fire button +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" + +- assertNotVisible: "https://example.com/" +- assertVisible: "Search or enter address" +- tapOn: "Cancel" +- tapOn: "Tab Switcher" +- assertNotVisible: "Example Domain" +- assertVisible: "1 Private Tab" +- tapOn: "Done" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" + +- inputText: "ex" +- assertNotVisible: "example.com" +- assertNotVisible: "Example Domain" + +- pressKey: Backspace +- pressKey: Backspace + +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter +- assertVisible: "Storage Counter: undefined" +- assertVisible: "Cookie Counter:" + +# Clear from Tab Switcher + +- tapOn: "Tab Switcher" +- assertVisible: "1 Private Tab" +- tapOn: "Close all tabs and clear data" +- tapOn: "Close Tabs and Clear Data" + +- assertNotVisible: "https://privacy-test-pages.site/features/local-storage.html" +- assertVisible: "Search or enter address" +- tapOn: "Cancel" +- tapOn: "Tab Switcher" +- assertNotVisible: "Example Domain" +- assertVisible: "1 Private Tab" +- tapOn: "Done" + + + + diff --git a/.maestro/run_ui_tests.sh b/.maestro/run_ui_tests.sh index 1556b9d83c..5a4f7b1fa6 100755 --- a/.maestro/run_ui_tests.sh +++ b/.maestro/run_ui_tests.sh @@ -78,7 +78,6 @@ echo "ℹ️ using device $device_uuid" # Simulator should already be up and running from running the setup script # re-run the setup script with `--skip-build` to set up again - echo "ℹ️ creating run log in $run_log" if [ -f $run_log ]; then rm $run_log From 05b0c98d0b5dae40c031e03db93cef69c8acce3a Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Mon, 15 Jul 2024 12:48:05 +0100 Subject: [PATCH 24/48] bump toolbox (#3079) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/SyncUI/Package.swift | 2 +- LocalPackages/Waitlist/Package.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23a2eb6ebc..5f0fc9028a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10134,7 +10134,7 @@ repositoryURL = "https://github.com/duckduckgo/apple-toolbox.git"; requirement = { kind = exactVersion; - version = 3.1.1; + version = 3.1.2; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 16c1672459..1a241e12ad 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/apple-toolbox.git", "state" : { - "revision" : "ab53ca41e9044a20eab7e53249526fadcf9acc9f", - "version" : "3.1.1" + "revision" : "0c13c5f056805f2d403618ccc3bfb833c303c68d", + "version" : "3.1.2" } }, { diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 72196aeadb..d1da2db921 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -34,7 +34,7 @@ let package = Package( dependencies: [ .package(path: "../DuckUI"), .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.1"), + .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ .target( diff --git a/LocalPackages/Waitlist/Package.swift b/LocalPackages/Waitlist/Package.swift index 66c84034d6..59aa29a33f 100644 --- a/LocalPackages/Waitlist/Package.swift +++ b/LocalPackages/Waitlist/Package.swift @@ -16,7 +16,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.1"), + .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ .target( From a0267674a9942a117a57b0398388bb091777cdf1 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 15 Jul 2024 15:06:01 +0200 Subject: [PATCH 25/48] Remove deprecated pixel autofillSettingsOpened (#3077) Task/Issue URL: https://app.asana.com/0/1203822806345703/1207774062910897/f Tech Design URL: CC: Description: Removes deprecated autofill settings pixel (m_autofill_settings_opened) --- Core/PixelEvent.swift | 2 -- DuckDuckGo/SettingsViewModel.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 28fd27f3ba..5478cdfe88 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -242,7 +242,6 @@ extension Pixel { case autofillLoginsFillLoginInlineDisablePromptAutofillKept case autofillLoginsFillLoginInlineDisablePromptAutofillDisabled - case autofillSettingsOpened case autofillLoginsSettingsEnabled case autofillLoginsSettingsDisabled case autofillLoginsSettingsResetExcludedDisplayed @@ -982,7 +981,6 @@ extension Pixel.Event { case .autofillLoginsFillLoginInlineDisablePromptAutofillKept: return "m_autofill_logins_save_disable-prompt_autofill-kept" case .autofillLoginsFillLoginInlineDisablePromptAutofillDisabled: return "m_autofill_logins_save_disable-prompt_autofill-disabled" - case .autofillSettingsOpened: return "m_autofill_settings_opened" case .autofillLoginsSettingsEnabled: return "m_autofill_logins_settings_enabled" case .autofillLoginsSettingsDisabled: return "m_autofill_logins_settings_disabled" case .autofillLoginsSettingsResetExcludedDisplayed: return "m_autofill_settings_reset_excluded_displayed" diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 33720c1dd0..bf1ac1c903 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -581,7 +581,6 @@ extension SettingsViewModel { case .feedback: presentViewController(legacyViewProvider.feedback, modal: false) case .logins: - firePixel(.autofillSettingsOpened) pushViewController(legacyViewProvider.loginSettings(delegate: self, selectedAccount: state.activeWebsiteAccount)) From 0daaa5d5fe7d6acfcbda9a88441a637bbe5a9a77 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Mon, 15 Jul 2024 14:25:40 +0100 Subject: [PATCH 26/48] Update BSK for C-S-S changes related to DuckPlayer (#3078) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5f0fc9028a..475900bc1d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10118,7 +10118,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 171.1.0; + version = 171.2.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1a241e12ad..3eea7ded49 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "c9462f5a01ef4b298caea661981fa6cecff687b7", - "version" : "171.1.0" + "revision" : "a3bd039e78e75e0ad36dffbf1874b555738f22af", + "version" : "171.2.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "9c65477457126ab7ad963a32b7f85ce08e6bd1a7", - "version" : "6.0.0" + "revision" : "d0d4f28bbe2fd44cbc5db5f109bb8453699308a3", + "version" : "6.1.0" } }, { From dcf747c8a1341a117f72272161a2ee7f62a4faa7 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 15 Jul 2024 18:39:54 +0200 Subject: [PATCH 27/48] Release 7.129.0-0 (#3082) --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 37 ++++++++---- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++++++---------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- fastlane/metadata/default/release_notes.txt | 3 +- 6 files changed, 59 insertions(+), 45 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index cd6762ec6d..4e407b2295 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.128.0 +MARKETING_VERSION = 7.129.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index fdeb22501d..e6d02a580e 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"889258698061231074e85e982257c377\"" - public static let embeddedDataSHA = "7e915bd1830ef7762b731e62a61c9c3f53f79e2f247d169dd70b45b1121f93d0" + public static let embeddedDataETag = "\"d73997ee1028028a4259dec5f9c4beca\"" + public static let embeddedDataSHA = "2810166895dee6bfebfc57d6dc041ef008bf85f9346e541c963d63d8fa1dc2dc" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index d910ca5022..c5d0760424 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1720185364708, + "version": 1720796469387, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -320,6 +320,9 @@ { "domain": "lotusbakeries.com" }, + { + "domain": "harley-davidson.com" + }, { "domain": "marvel.com" }, @@ -361,7 +364,7 @@ } } }, - "hash": "c47027bc9d235b909d868f5ffa97d3c9" + "hash": "df9028f0ff7582513c9660a63b0eb86b" }, "autofill": { "exceptions": [ @@ -1164,9 +1167,6 @@ { "domain": "soranews24.com" }, - { - "domain": "pointstreaksites.com" - }, { "domain": "marvel.com" }, @@ -1177,7 +1177,7 @@ "domain": "noaprints.com" } ], - "hash": "477f58bcbc508d5ae1e73941407284b7" + "hash": "ee9d383becfe28d8a5b575b51f5c9d11" }, "cookie": { "settings": { @@ -1302,6 +1302,9 @@ }, "autoplay": { "state": "disabled" + }, + "openInNewTab": { + "state": "disabled" } }, "settings": { @@ -1368,7 +1371,7 @@ ] }, "state": "disabled", - "hash": "d950da7e94382e26e5da62e52ce3ac79" + "hash": "17689d71b3f20a31d01c050bd5cba1ee" }, "elementHiding": { "exceptions": [ @@ -3793,7 +3796,7 @@ "type": "hide" }, { - "selector": "[devicetype=\"desktop\"] .grid:not([style='filter: blur(4px);']) ~ shreddit-experience-tree", + "selector": "[devicetype=\"desktop\"] .grid:not([style='filter: blur(4px);']) ~ shreddit-experience-tree:not([active-experiences='[\"nsfw_bypassable\"]'])", "type": "hide" } ] @@ -4012,6 +4015,14 @@ { "selector": "[class^='styles__PubAdWrapper']", "type": "closest-empty" + }, + { + "selector": "[data-test='sponsored-text']", + "type": "hide-empty" + }, + { + "selector": "[id^='btf-']", + "type": "closest-empty" } ] }, @@ -4570,7 +4581,7 @@ ] }, "state": "enabled", - "hash": "573f3bc0a0f15e5dc7d561034e7a082d" + "hash": "c69c499b65697c42f660e3c5588bebe4" }, "exceptionHandler": { "exceptions": [ @@ -5078,12 +5089,15 @@ }, { "percent": 50 + }, + { + "percent": 100 } ] } } }, - "hash": "8ec044ab0b9532ae2b9852c70fd0d436" + "hash": "e705c70baeee9f16f5c481a5be1d95b9" }, "https": { "state": "enabled", @@ -6957,6 +6971,7 @@ "newschannel20.com", "newschannel9.com", "okcfox.com", + "pointstreaksites.com", "post-gazette.com", "raleighcw.com", "siouxlandnews.com", @@ -8677,7 +8692,7 @@ "domain": "noaprints.com" } ], - "hash": "d7d18b84410856df8bf3fb5e2a1f2e56" + "hash": "0b185cf5671fe879e5029509a627c457" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 475900bc1d..679f2e09ed 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8341,7 +8341,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8378,7 +8378,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8468,7 +8468,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8495,7 +8495,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8644,7 +8644,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8669,7 +8669,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8738,7 +8738,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8772,7 +8772,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8805,7 +8805,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8835,7 +8835,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9145,7 +9145,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9176,7 +9176,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9204,7 +9204,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9237,7 +9237,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9267,7 +9267,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9300,11 +9300,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9537,7 +9537,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9564,7 +9564,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9596,7 +9596,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9633,7 +9633,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9668,7 +9668,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9703,11 +9703,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9880,11 +9880,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9913,10 +9913,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 25d6870138..4645ea5adf 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.128.0 + 7.129.0 Key version Title diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index aab259d4e4..098fd1666f 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,2 +1 @@ -- Fixed an issue where cancelling downloads would sometimes not work as expected. -- Make sure you have iOS 15 or later to keep getting the latest browser updates and improvements. Here's how to update iOS: https://support.apple.com/guide/iphone/update-ios-iph3e504502/14.0/ios/14.0 +- Bug fixes and other improvements. \ No newline at end of file From a1703683b6478a6fcfd701b993c302a1bc9d1f76 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 16 Jul 2024 18:07:49 +0100 Subject: [PATCH 28/48] Update BSK version (#3087) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 679f2e09ed..a96329a2bb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10118,7 +10118,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 171.2.0; + version = 171.2.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3eea7ded49..ca5f14f5c0 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "a3bd039e78e75e0ad36dffbf1874b555738f22af", - "version" : "171.2.0" + "revision" : "09844ec2d0c9d2312e02c90527e8df063db89318", + "version" : "171.2.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "d0d4f28bbe2fd44cbc5db5f109bb8453699308a3", - "version" : "6.1.0" + "revision" : "dc26bfc6e33ad9c79a719b7f21d5ca0564db1859", + "version" : "6.3.0" } }, { From bc393173118690fe93476e9fae31fd0fb548fbd8 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Jul 2024 23:33:58 -0400 Subject: [PATCH 29/48] Fix tests --- DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift b/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift index 0eddd7cc16..a39fa50e97 100644 --- a/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift +++ b/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift @@ -19,6 +19,7 @@ import XCTest import DDGSync +import DDGSyncTestingUtilities import Combine @testable import Core From df020773c5c7aa02b6a149244d92336a18e66504 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Jul 2024 23:36:29 -0400 Subject: [PATCH 30/48] Revert "Fix tests" This reverts commit bc393173118690fe93476e9fae31fd0fb548fbd8. --- DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift b/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift index a39fa50e97..0eddd7cc16 100644 --- a/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift +++ b/DuckDuckGoTests/SyncErrorHandlerSyncErrorsAlertsTests.swift @@ -19,7 +19,6 @@ import XCTest import DDGSync -import DDGSyncTestingUtilities import Combine @testable import Core From 866d9f7e99978183418a0873dd93d4fdb52c1d64 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 17 Jul 2024 15:35:00 +0200 Subject: [PATCH 31/48] Update BSK to latest - for macOS fix to AdAttribution (#3084) Task/Issue URL: https://app.asana.com/0/856498667320406/1207772445672485/f Tech Design URL: CC: Description: Update BSK to latest - for macOS fix to AdAttribution Steps to test this PR: Should not affect anything. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a96329a2bb..a53328ddad 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10118,7 +10118,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 171.2.1; + version = 171.2.2; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { From 436449cbabbaf6d5bc060035c51a7e90631ba7b3 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 17 Jul 2024 21:06:22 +0200 Subject: [PATCH 32/48] Expand AdHoc build workflow, add debug bookmarks screen (#3086) Task/Issue URL: https://app.asana.com/0/414235014887631/1207821981275160/f https://app.asana.com/0/856498667320406/1207823615637251/f Description: Expand AdHoc build workflow to have an option of alpha and regular builds. Add debug bookmarks screen. Steps to test this PR: Check the workflow file for any obvious mistakes, validate history of executions for any mistakes. Validate debug screen works as expected. --- .github/workflows/adhoc.yml | 17 +++- DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/BookmarksDebugViewController.swift | 96 +++++++++++++++++++ DuckDuckGo/Debug.storyboard | 81 +++++++++++----- fastlane/Matchfile | 6 ++ 5 files changed, 178 insertions(+), 26 deletions(-) create mode 100644 DuckDuckGo/BookmarksDebugViewController.swift diff --git a/.github/workflows/adhoc.yml b/.github/workflows/adhoc.yml index 6233aefe1f..baca7fb705 100644 --- a/.github/workflows/adhoc.yml +++ b/.github/workflows/adhoc.yml @@ -10,6 +10,14 @@ on: description: "Asana task URL" required: false type: string + build-type: + description: "Build Configuration" + type: choice + required: true + default: 'Alpha' + options: + - Alpha + - Release jobs: make-adhoc: @@ -41,10 +49,15 @@ jobs: APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} run: | + lane_to_use="adhoc" + if [[ "${{ github.event.inputs.build-type }}" == "Release" ]]; then + lane_to_use = "release_adhoc" + fi + if [[ -n "${{ github.event.inputs.suffix }}" ]]; then - bundle exec fastlane adhoc suffix:${{ github.event.inputs.suffix }} + bundle exec fastlane ${lane_to_use} suffix:${{ github.event.inputs.suffix }} else - bundle exec fastlane adhoc + bundle exec fastlane ${lane_to_use} fi - name: Set filenames diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a53328ddad..66d3f24b0e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -576,6 +576,7 @@ 98A16C2D28A11D6200A6C003 /* BrowserServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 98A16C2C28A11D6200A6C003 /* BrowserServicesKit */; }; 98A50962294B48A400D10880 /* Bookmarks in Frameworks */ = {isa = PBXBuildFile; productRef = 98A50961294B48A400D10880 /* Bookmarks */; }; 98A54A8422AFCB2D00E541F4 /* Instruments.instrpkg in Sources */ = {isa = PBXBuildFile; fileRef = 98A54A8322AFCB2D00E541F4 /* Instruments.instrpkg */; }; + 98A860EF2C4682E00077FE4D /* BookmarksDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A860EE2C4682E00077FE4D /* BookmarksDebugViewController.swift */; }; 98AA92B32456FBE100ED4B9E /* SearchFieldContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AA92B22456FBE100ED4B9E /* SearchFieldContainerView.swift */; }; 98AAF8E4292EB46000DBDF06 /* BookmarksMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AAF8E3292EB46000DBDF06 /* BookmarksMigrationTests.swift */; }; 98B000532915C46E0034BCA0 /* LegacyBookmarksStoreMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B000522915C46E0034BCA0 /* LegacyBookmarksStoreMigration.swift */; }; @@ -2199,6 +2200,7 @@ 989B337422D7EF2100437824 /* EmptyCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyCollectionReusableView.swift; sourceTree = ""; }; 98A54A8122AFCB2C00E541F4 /* Instruments.instrdst */ = {isa = PBXFileReference; explicitFileType = com.apple.instruments.instrdst; includeInIndex = 0; path = Instruments.instrdst; sourceTree = BUILT_PRODUCTS_DIR; }; 98A54A8322AFCB2D00E541F4 /* Instruments.instrpkg */ = {isa = PBXFileReference; lastKnownFileType = "com.apple.instruments.package-definition"; path = Instruments.instrpkg; sourceTree = ""; }; + 98A860EE2C4682E00077FE4D /* BookmarksDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDebugViewController.swift; sourceTree = ""; }; 98AA92B22456FBE100ED4B9E /* SearchFieldContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFieldContainerView.swift; sourceTree = ""; }; 98AAF8E3292EB46000DBDF06 /* BookmarksMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksMigrationTests.swift; sourceTree = ""; }; 98AC5D8B251EAC07009B7979 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -3936,6 +3938,7 @@ EE72CA842A862D000043B5B3 /* NetworkProtectionDebugViewController.swift */, CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */, 851624C62B96389D002D5CD7 /* HistoryDebugViewController.swift */, + 98A860EE2C4682E00077FE4D /* BookmarksDebugViewController.swift */, ); name = Debug; sourceTree = ""; @@ -6963,6 +6966,7 @@ 31C70B5B2804C61000FB6AD1 /* SaveAutofillLoginManager.swift in Sources */, 982123502B6D233E00F08C57 /* UserSession.swift in Sources */, 85449EFD23FDA71F00512AAF /* KeyboardSettings.swift in Sources */, + 98A860EF2C4682E00077FE4D /* BookmarksDebugViewController.swift in Sources */, 980891A222369ADB00313A70 /* FeedbackUserText.swift in Sources */, 1DEAADFF2BA7832F00E25A97 /* EmailProtectionView.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, diff --git a/DuckDuckGo/BookmarksDebugViewController.swift b/DuckDuckGo/BookmarksDebugViewController.swift new file mode 100644 index 0000000000..66cfe15e51 --- /dev/null +++ b/DuckDuckGo/BookmarksDebugViewController.swift @@ -0,0 +1,96 @@ +// +// BookmarksDebugViewController.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import SwiftUI +import Core +import Combine +import Persistence +import Bookmarks +import CoreData + +class BookmarksDebugViewController: UIHostingController { + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: BookmarksDebugRootView()) + } + +} + +struct BookmarksDebugRootView: View { + + @ObservedObject var model = BookmarksDebugViewModel() + + var body: some View { + List(model.bookmarks, id: \.id) { entry in + VStack(alignment: .leading) { + Text(entry.title ?? "empty!") + .font(.system(size: 16)) + Text("Is unified fav: " + (entry.isFavorite(on: .unified) ? "true" : "false") ) + .font(.system(size: 12)) + Text("Is mobile fav: " + (entry.isFavorite(on: .mobile) ? "true" : "false") ) + .font(.system(size: 12)) + Text("Is desktop fav: " + (entry.isFavorite(on: .desktop) ? "true" : "false") ) + .font(.system(size: 12)) + ForEach(model.bookmarkAttributes, id: \.self) { attr in + Text(entry.formattedValue(for: attr)) + .font(.system(size: 12)) + } + + } + } + .navigationTitle("\(model.bookmarks.count) Bookmarks") + } + +} + +extension BookmarkEntity { + + func formattedValue(for key: String) -> String { + key + ": \'" + String(describing: value(forKey: key)) + "'" + } +} + +class BookmarksDebugViewModel: ObservableObject { + + @Published var bookmarks = [BookmarkEntity]() + let bookmarkAttributes: [String] + + let database: CoreDataDatabase + let context: NSManagedObjectContext + + init() { + database = BookmarksDatabase.make() + database.loadStore() + + context = database.makeContext(concurrencyType: .mainQueueConcurrencyType) + bookmarkAttributes = Array(BookmarkEntity.entity(in: context).attributesByName.keys) + + fetch() + } + + func fetch() { + + let fetchRequest = BookmarkEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(BookmarkEntity.title), + ascending: false)] + fetchRequest.returnsObjectsAsFaults = false + bookmarks = (try? context.fetch(fetchRequest)) ?? [] + } +} diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index 9d62963206..2ae12b72f9 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -143,7 +143,7 @@ - - + - + + + + + + + + + + + + @@ -177,7 +194,7 @@ - + @@ -197,7 +214,7 @@ - + @@ -217,7 +234,7 @@ - + @@ -229,7 +246,7 @@ - + @@ -241,7 +258,7 @@ - + @@ -250,7 +267,7 @@ - + @@ -259,7 +276,7 @@ - + @@ -268,7 +285,7 @@ - + @@ -277,7 +294,7 @@ - + @@ -286,7 +303,7 @@ - + @@ -295,7 +312,7 @@ - + @@ -304,7 +321,7 @@ - + @@ -313,7 +330,7 @@ - + @@ -322,7 +339,7 @@ - + @@ -878,17 +895,17 @@ - + - + - + - + @@ -946,6 +963,22 @@ + + + + + + + + + + + + + + + + diff --git a/fastlane/Matchfile b/fastlane/Matchfile index eb39fd637c..022ff22969 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -26,6 +26,12 @@ for_lane :adhoc do template_name "Default Web Browser iOS (Dist)" end +for_lane :release_adhoc do + type "adhoc" + force_for_new_devices true + template_name "Default Web Browser iOS (Dist)" +end + for_lane :alpha_adhoc do type "adhoc" app_identifier ["com.duckduckgo.mobile.ios.alpha", "com.duckduckgo.mobile.ios.alpha.ShareExtension", "com.duckduckgo.mobile.ios.alpha.OpenAction2", "com.duckduckgo.mobile.ios.alpha.Widgets", "com.duckduckgo.mobile.ios.alpha.NetworkExtension"] From 4a7ffd9a82fd83bb5c1fe8c33a5e1f060518c5d0 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 17 Jul 2024 21:18:14 +0200 Subject: [PATCH 33/48] AdHoc lane: Make proper assignment to variable (#3095) Task/Issue URL: https://app.asana.com/0/856498667320406/1207823615637251/f Tech Design URL: CC: Description: AdHoc lane: Make proper assignment to variable --- .github/workflows/adhoc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/adhoc.yml b/.github/workflows/adhoc.yml index baca7fb705..157bf4371f 100644 --- a/.github/workflows/adhoc.yml +++ b/.github/workflows/adhoc.yml @@ -51,7 +51,7 @@ jobs: run: | lane_to_use="adhoc" if [[ "${{ github.event.inputs.build-type }}" == "Release" ]]; then - lane_to_use = "release_adhoc" + lane_to_use="release_adhoc" fi if [[ -n "${{ github.event.inputs.suffix }}" ]]; then From 4b865ea877a1e7b25a98eaffca25864cfa5db3a7 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 18 Jul 2024 12:44:10 +0100 Subject: [PATCH 34/48] fix ui tests broken by new onboarding and use shared setup flow (#3081) Task/Issue URL: https://app.asana.com/0/392891325557410/1207808692245660/f Tech Design URL: CC: Description: Also: https://app.asana.com/0/392891325557410/1207809241818381/f Fixes UI test breakage by skipping assertion for title of onboarding screen. Steps to test this PR: Confirm E2E tests pass: https://github.com/duckduckgo/iOS/actions/runs/9974333207 Confirm Sync tests pass: https://github.com/duckduckgo/iOS/actions/runs/9974343415 Run the .maestro/setup_ui_tests.sh script. Run some of the UI tests using the .maestro/run_ui_tests.sh script. Should consistently pass. --- ...tic_no_ad_domain_param_u3_param_included.yaml | 8 +------- ...ic_no_ad_domain_param_dsl_param_included.yaml | 10 ++-------- ..._no_ad_domain_param_but_missing_u3_param.yaml | 8 +------- ...no_ad_domain_param_but_missing_dsl_param.yaml | 8 +------- ..._domain_provided_but_empty_u3_not_needed.yaml | 8 +------- ...domain_provided_but_empty_dsl_not_needed.yaml | 10 +++------- ...rovided_ad_domain_provided_u3_not_needed.yaml | 10 +++------- ...ovided_ad_domain_provided_dsl_not_needed.yaml | 12 ++++-------- ...ain_provided_but_incorrect_u3_not_needed.yaml | 9 ++------- ...in_provided_but_incorrect_dsl_not_needed.yaml | 9 ++------- ...vided_but_its_not_a_domain_u3_not_needed.yaml | 9 ++------- ...ided_but_its_not_a_domain_dsl_not_needed.yaml | 9 ++------- ..._a_subdomain_of_advertiser_u3_not_needed.yaml | 9 ++------- ...a_subdomain_of_advertiser_dsl_not_needed.yaml | 9 ++------- .maestro/browser_features/opening_tabs.yaml | 12 +++--------- .maestro/browser_features/search_bar.yaml | 12 +++--------- .maestro/browser_features/swipe_tabs.yaml | 12 +++--------- .../data_clearing_tests/01_fire_proofing.yml | 8 +------- .../02_duckduckgo_settings.yml | 11 ++--------- .../01_single-site_single-tab_session.yaml | 8 +------- .../02_single-site_new_tab_session.yaml | 8 +------- ..._single-site_new-tab_session_variant_two.yaml | 8 +------- .../04_single-site_multi-tab_session.yaml | 8 +------- .../05_multi-site_single-tab_session.yaml | 8 +------- .maestro/privacy_tests/06_multi-tab.yaml | 8 +------- .../07_browser_restart_mid-session.yaml | 8 +------- .../08_navigation_with_back_forward.yaml | 8 +------- .../09_navigation_with_refresh.yaml | 8 +------- .maestro/release_tests/autoclear.yaml | 5 +++-- .maestro/release_tests/backgrounding.yaml | 5 +---- .maestro/release_tests/bookmarks-multi.yaml | 4 +--- .maestro/release_tests/bookmarks.yaml | 4 +--- .maestro/release_tests/browsing.yaml | 10 ++-------- .maestro/release_tests/content-blocking.yaml | 4 +--- .maestro/release_tests/emailprotection.yaml | 10 ++-------- .maestro/release_tests/favorites.yaml | 4 +--- .maestro/release_tests/firebutton.yaml | 6 +----- .../release_tests/password-authentication.yaml | 4 +--- .maestro/release_tests/password-autofill.yaml | 4 +--- .maestro/release_tests/password-management.yaml | 4 +--- .maestro/release_tests/tabs.yaml | 8 +------- .maestro/release_tests/widgets.yaml | 4 +--- .../1_-_AddressBarSpoof,_basicauth.yaml | 9 ++------- .../2_-_AddressBarSpoof,_aboutblank.yaml | 9 ++------- .../3_-_AddressBarSpoof,_appschemes.yaml | 9 ++------- .../4_-_AddressBarSpoof,_b64_html.yaml | 9 ++------- .../5_-_AddressBarSpoof,_downloadpath.yaml | 9 ++------- .../6_-_AddressBarSpoof,_formaction.yaml | 9 ++------- .../7_-_AddressBarSpoof,_pagerewrite.yaml | 9 ++------- .maestro/shared/onboarding.yaml | 4 +++- .maestro/shared/setup.yaml | 16 ++++++++++++++++ .maestro/sync_tests/01_create_account.yaml | 12 ++---------- .maestro/sync_tests/02_login_account.yaml | 12 ++---------- .maestro/sync_tests/03_recover_account.yaml | 12 ++---------- .maestro/sync_tests/04_sync_data_setup.yaml | 12 ++---------- .maestro/sync_tests/05_sync_data_check.yaml | 11 ++--------- .maestro/sync_tests/06_delete_account.yaml | 12 ++---------- .../xcshareddata/xcschemes/DuckDuckGo.xcscheme | 8 ++++---- 58 files changed, 116 insertions(+), 377 deletions(-) create mode 100644 .maestro/shared/setup.yaml diff --git a/.maestro/ad_click_detection_flow_tests/01_yjs_heuristic_no_ad_domain_param_u3_param_included.yaml b/.maestro/ad_click_detection_flow_tests/01_yjs_heuristic_no_ad_domain_param_u3_param_included.yaml index 9765f58cab..662424f7f2 100644 --- a/.maestro/ad_click_detection_flow_tests/01_yjs_heuristic_no_ad_domain_param_u3_param_included.yaml +++ b/.maestro/ad_click_detection_flow_tests/01_yjs_heuristic_no_ad_domain_param_u3_param_included.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/02_mjs_heuristic_no_ad_domain_param_dsl_param_included.yaml b/.maestro/ad_click_detection_flow_tests/02_mjs_heuristic_no_ad_domain_param_dsl_param_included.yaml index 25b2f615f9..dbd33d601e 100644 --- a/.maestro/ad_click_detection_flow_tests/02_mjs_heuristic_no_ad_domain_param_dsl_param_included.yaml +++ b/.maestro/ad_click_detection_flow_tests/02_mjs_heuristic_no_ad_domain_param_dsl_param_included.yaml @@ -5,15 +5,9 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml - + file: ../shared/setup.yaml + # Load Site - assertVisible: id: "searchEntry" diff --git a/.maestro/ad_click_detection_flow_tests/03_yjs_heuristic_no_ad_domain_param_but_missing_u3_param.yaml b/.maestro/ad_click_detection_flow_tests/03_yjs_heuristic_no_ad_domain_param_but_missing_u3_param.yaml index 7f624b5bba..cee80591ef 100644 --- a/.maestro/ad_click_detection_flow_tests/03_yjs_heuristic_no_ad_domain_param_but_missing_u3_param.yaml +++ b/.maestro/ad_click_detection_flow_tests/03_yjs_heuristic_no_ad_domain_param_but_missing_u3_param.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/04_mjs_heuristic_no_ad_domain_param_but_missing_dsl_param.yaml b/.maestro/ad_click_detection_flow_tests/04_mjs_heuristic_no_ad_domain_param_but_missing_dsl_param.yaml index 0c708a1a9c..f0a4487522 100644 --- a/.maestro/ad_click_detection_flow_tests/04_mjs_heuristic_no_ad_domain_param_but_missing_dsl_param.yaml +++ b/.maestro/ad_click_detection_flow_tests/04_mjs_heuristic_no_ad_domain_param_but_missing_dsl_param.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/05_yjs_heuristic_ad_domain_provided_but_empty_u3_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/05_yjs_heuristic_ad_domain_provided_but_empty_u3_not_needed.yaml index a40e33dae0..e781172f37 100644 --- a/.maestro/ad_click_detection_flow_tests/05_yjs_heuristic_ad_domain_provided_but_empty_u3_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/05_yjs_heuristic_ad_domain_provided_but_empty_u3_not_needed.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/06_mjs_heuristic_ad_domain_provided_but_empty_dsl_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/06_mjs_heuristic_ad_domain_provided_but_empty_dsl_not_needed.yaml index f55bdff711..ca1deb1ce1 100644 --- a/.maestro/ad_click_detection_flow_tests/06_mjs_heuristic_ad_domain_provided_but_empty_dsl_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/06_mjs_heuristic_ad_domain_provided_but_empty_dsl_not_needed.yaml @@ -3,14 +3,10 @@ tags: - adClick --- -- clearState -- launchApp + +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/07_yjs_bing-provided_ad_domain_provided_u3_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/07_yjs_bing-provided_ad_domain_provided_u3_not_needed.yaml index 4d37ae8f4b..35179136a3 100644 --- a/.maestro/ad_click_detection_flow_tests/07_yjs_bing-provided_ad_domain_provided_u3_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/07_yjs_bing-provided_ad_domain_provided_u3_not_needed.yaml @@ -3,14 +3,10 @@ tags: - adClick --- -- clearState -- launchApp + +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/08_mjs_bing-provided_ad_domain_provided_dsl_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/08_mjs_bing-provided_ad_domain_provided_dsl_not_needed.yaml index 1443b604a9..87196d401d 100644 --- a/.maestro/ad_click_detection_flow_tests/08_mjs_bing-provided_ad_domain_provided_dsl_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/08_mjs_bing-provided_ad_domain_provided_dsl_not_needed.yaml @@ -3,14 +3,10 @@ tags: - adClick --- -- clearState -- launchApp -- runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + +# Set up +- runFlow: + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/09_yjs_bing-provided_ad_domain_provided_but_incorrect_u3_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/09_yjs_bing-provided_ad_domain_provided_but_incorrect_u3_not_needed.yaml index 672eb8e2b8..f15881ac1e 100644 --- a/.maestro/ad_click_detection_flow_tests/09_yjs_bing-provided_ad_domain_provided_but_incorrect_u3_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/09_yjs_bing-provided_ad_domain_provided_but_incorrect_u3_not_needed.yaml @@ -4,14 +4,9 @@ tags: --- -- clearState -- launchApp +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/10_mjs_bing-provided_ad_domain_provided_but_incorrect_dsl_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/10_mjs_bing-provided_ad_domain_provided_but_incorrect_dsl_not_needed.yaml index 479b962c3d..83de1274e4 100644 --- a/.maestro/ad_click_detection_flow_tests/10_mjs_bing-provided_ad_domain_provided_but_incorrect_dsl_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/10_mjs_bing-provided_ad_domain_provided_but_incorrect_dsl_not_needed.yaml @@ -4,14 +4,9 @@ tags: --- -- clearState -- launchApp +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/11_yjs_bing-provided_ad_domain_provided_but_its_not_a_domain_u3_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/11_yjs_bing-provided_ad_domain_provided_but_its_not_a_domain_u3_not_needed.yaml index 0fbc199b64..a3ef379d8b 100644 --- a/.maestro/ad_click_detection_flow_tests/11_yjs_bing-provided_ad_domain_provided_but_its_not_a_domain_u3_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/11_yjs_bing-provided_ad_domain_provided_but_its_not_a_domain_u3_not_needed.yaml @@ -4,14 +4,9 @@ tags: --- -- clearState -- launchApp +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/12_mjs_bing-provided_ad_domain_provided_but_its_not_a_domain_dsl_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/12_mjs_bing-provided_ad_domain_provided_but_its_not_a_domain_dsl_not_needed.yaml index f6a7a4816b..b1c1d7a17a 100644 --- a/.maestro/ad_click_detection_flow_tests/12_mjs_bing-provided_ad_domain_provided_but_its_not_a_domain_dsl_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/12_mjs_bing-provided_ad_domain_provided_but_its_not_a_domain_dsl_not_needed.yaml @@ -4,14 +4,9 @@ tags: --- -- clearState -- launchApp +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/13_yjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_u3_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/13_yjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_u3_not_needed.yaml index ca0834351f..c2dcc7ec5d 100644 --- a/.maestro/ad_click_detection_flow_tests/13_yjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_u3_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/13_yjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_u3_not_needed.yaml @@ -4,14 +4,9 @@ tags: --- -- clearState -- launchApp +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/ad_click_detection_flow_tests/14_mjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_dsl_not_needed.yaml b/.maestro/ad_click_detection_flow_tests/14_mjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_dsl_not_needed.yaml index e6bf86893c..f656ac2b1e 100644 --- a/.maestro/ad_click_detection_flow_tests/14_mjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_dsl_not_needed.yaml +++ b/.maestro/ad_click_detection_flow_tests/14_mjs_bing-provided_ad_domain_provided_but_its_a_subdomain_of_advertiser_dsl_not_needed.yaml @@ -4,14 +4,9 @@ tags: --- -- clearState -- launchApp +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/browser_features/opening_tabs.yaml b/.maestro/browser_features/opening_tabs.yaml index 8b2aa60e1a..4043503c40 100644 --- a/.maestro/browser_features/opening_tabs.yaml +++ b/.maestro/browser_features/opening_tabs.yaml @@ -5,15 +5,9 @@ tags: --- -# Set up -- clearState -- launchApp -- runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml +# Set up +- runFlow: + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/browser_features/search_bar.yaml b/.maestro/browser_features/search_bar.yaml index a8708011ef..02c07cc530 100644 --- a/.maestro/browser_features/search_bar.yaml +++ b/.maestro/browser_features/search_bar.yaml @@ -5,15 +5,9 @@ tags: --- -# Set up -- clearState -- launchApp -- runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml +# Set up +- runFlow: + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/browser_features/swipe_tabs.yaml b/.maestro/browser_features/swipe_tabs.yaml index 4f79623a01..e817ac9e29 100644 --- a/.maestro/browser_features/swipe_tabs.yaml +++ b/.maestro/browser_features/swipe_tabs.yaml @@ -5,15 +5,9 @@ tags: --- -# Set up -- clearState -- launchApp -- runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml +# Set up +- runFlow: + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/data_clearing_tests/01_fire_proofing.yml b/.maestro/data_clearing_tests/01_fire_proofing.yml index 26b0180ecd..6e06bb8e7d 100644 --- a/.maestro/data_clearing_tests/01_fire_proofing.yml +++ b/.maestro/data_clearing_tests/01_fire_proofing.yml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/data_clearing_tests/02_duckduckgo_settings.yml b/.maestro/data_clearing_tests/02_duckduckgo_settings.yml index fa68b0c05a..3a4ef8e186 100644 --- a/.maestro/data_clearing_tests/02_duckduckgo_settings.yml +++ b/.maestro/data_clearing_tests/02_duckduckgo_settings.yml @@ -5,15 +5,8 @@ tags: --- # Set up -- clearKeychain -- clearState -- launchApp -- runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml +- runFlow: + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/01_single-site_single-tab_session.yaml b/.maestro/privacy_tests/01_single-site_single-tab_session.yaml index bfb22d5973..23c4e48d96 100644 --- a/.maestro/privacy_tests/01_single-site_single-tab_session.yaml +++ b/.maestro/privacy_tests/01_single-site_single-tab_session.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/02_single-site_new_tab_session.yaml b/.maestro/privacy_tests/02_single-site_new_tab_session.yaml index c6b72bf028..b7e5a71cd6 100644 --- a/.maestro/privacy_tests/02_single-site_new_tab_session.yaml +++ b/.maestro/privacy_tests/02_single-site_new_tab_session.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/03_single-site_new-tab_session_variant_two.yaml b/.maestro/privacy_tests/03_single-site_new-tab_session_variant_two.yaml index 3d56674a16..4f7d2b02b6 100644 --- a/.maestro/privacy_tests/03_single-site_new-tab_session_variant_two.yaml +++ b/.maestro/privacy_tests/03_single-site_new-tab_session_variant_two.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/04_single-site_multi-tab_session.yaml b/.maestro/privacy_tests/04_single-site_multi-tab_session.yaml index 071716d320..b0b4771a7e 100644 --- a/.maestro/privacy_tests/04_single-site_multi-tab_session.yaml +++ b/.maestro/privacy_tests/04_single-site_multi-tab_session.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/05_multi-site_single-tab_session.yaml b/.maestro/privacy_tests/05_multi-site_single-tab_session.yaml index 34afabb5cc..68d9ef3f85 100644 --- a/.maestro/privacy_tests/05_multi-site_single-tab_session.yaml +++ b/.maestro/privacy_tests/05_multi-site_single-tab_session.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/06_multi-tab.yaml b/.maestro/privacy_tests/06_multi-tab.yaml index b55aa36cbc..e3bd769e46 100644 --- a/.maestro/privacy_tests/06_multi-tab.yaml +++ b/.maestro/privacy_tests/06_multi-tab.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/07_browser_restart_mid-session.yaml b/.maestro/privacy_tests/07_browser_restart_mid-session.yaml index e0e8cf0516..47b49d852f 100644 --- a/.maestro/privacy_tests/07_browser_restart_mid-session.yaml +++ b/.maestro/privacy_tests/07_browser_restart_mid-session.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/08_navigation_with_back_forward.yaml b/.maestro/privacy_tests/08_navigation_with_back_forward.yaml index c8cbd3f381..ada5d2c81e 100644 --- a/.maestro/privacy_tests/08_navigation_with_back_forward.yaml +++ b/.maestro/privacy_tests/08_navigation_with_back_forward.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/privacy_tests/09_navigation_with_refresh.yaml b/.maestro/privacy_tests/09_navigation_with_refresh.yaml index 30d58f8db4..27920928d5 100644 --- a/.maestro/privacy_tests/09_navigation_with_refresh.yaml +++ b/.maestro/privacy_tests/09_navigation_with_refresh.yaml @@ -5,14 +5,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/autoclear.yaml b/.maestro/release_tests/autoclear.yaml index aca8dfed81..45a7c564b9 100644 --- a/.maestro/release_tests/autoclear.yaml +++ b/.maestro/release_tests/autoclear.yaml @@ -5,10 +5,11 @@ tags: --- -# Set up -- clearState +# Set up - custom setup - launchApp: appId: "com.duckduckgo.mobile.ios" + clearState: true + clearKeychain: true arguments: "autoclear-ui-test": true diff --git a/.maestro/release_tests/backgrounding.yaml b/.maestro/release_tests/backgrounding.yaml index 808880b347..b60d54572f 100644 --- a/.maestro/release_tests/backgrounding.yaml +++ b/.maestro/release_tests/backgrounding.yaml @@ -6,11 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/bookmarks-multi.yaml b/.maestro/release_tests/bookmarks-multi.yaml index 97905d9eba..bc83790f8b 100644 --- a/.maestro/release_tests/bookmarks-multi.yaml +++ b/.maestro/release_tests/bookmarks-multi.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/bookmarks.yaml b/.maestro/release_tests/bookmarks.yaml index 0c320c684f..99bd798289 100644 --- a/.maestro/release_tests/bookmarks.yaml +++ b/.maestro/release_tests/bookmarks.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/browsing.yaml b/.maestro/release_tests/browsing.yaml index f191138307..83bf61d288 100644 --- a/.maestro/release_tests/browsing.yaml +++ b/.maestro/release_tests/browsing.yaml @@ -6,15 +6,9 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml - + file: ../shared/setup.yaml + # Load Site - assertVisible: id: "searchEntry" diff --git a/.maestro/release_tests/content-blocking.yaml b/.maestro/release_tests/content-blocking.yaml index 51c0a7aaf4..c4e4795260 100644 --- a/.maestro/release_tests/content-blocking.yaml +++ b/.maestro/release_tests/content-blocking.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/emailprotection.yaml b/.maestro/release_tests/emailprotection.yaml index 1b4f0a4326..d976f032f3 100644 --- a/.maestro/release_tests/emailprotection.yaml +++ b/.maestro/release_tests/emailprotection.yaml @@ -4,15 +4,9 @@ tags: --- -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml - + file: ../shared/setup.yaml + - tapOn: Settings # Handling two different flows because of the current experiment # TODO: Remove the unused flow when the experiment is completed. diff --git a/.maestro/release_tests/favorites.yaml b/.maestro/release_tests/favorites.yaml index 739f716695..a59482f511 100644 --- a/.maestro/release_tests/favorites.yaml +++ b/.maestro/release_tests/favorites.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/firebutton.yaml b/.maestro/release_tests/firebutton.yaml index 0cdff6e4a8..27bed9f729 100644 --- a/.maestro/release_tests/firebutton.yaml +++ b/.maestro/release_tests/firebutton.yaml @@ -6,12 +6,8 @@ tags: --- # Set up -- clearState -- clearKeychain -- launchApp - - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/password-authentication.yaml b/.maestro/release_tests/password-authentication.yaml index 8722ecd798..dfe82a507c 100644 --- a/.maestro/release_tests/password-authentication.yaml +++ b/.maestro/release_tests/password-authentication.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Validate passcode requested when accessing passwords for the first time - tapOn: "Settings" diff --git a/.maestro/release_tests/password-autofill.yaml b/.maestro/release_tests/password-autofill.yaml index 95a9f675f4..9db1875339 100644 --- a/.maestro/release_tests/password-autofill.yaml +++ b/.maestro/release_tests/password-autofill.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Capture login credentials - assertVisible: diff --git a/.maestro/release_tests/password-management.yaml b/.maestro/release_tests/password-management.yaml index 5191c2254f..79edf8a313 100644 --- a/.maestro/release_tests/password-management.yaml +++ b/.maestro/release_tests/password-management.yaml @@ -6,10 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Validate passcode requested when accessing passwords for the first time - tapOn: "Settings" diff --git a/.maestro/release_tests/tabs.yaml b/.maestro/release_tests/tabs.yaml index fd9b7460fe..23df750e99 100644 --- a/.maestro/release_tests/tabs.yaml +++ b/.maestro/release_tests/tabs.yaml @@ -6,14 +6,8 @@ tags: --- # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/release_tests/widgets.yaml b/.maestro/release_tests/widgets.yaml index a83460dbb0..4f5c08d819 100644 --- a/.maestro/release_tests/widgets.yaml +++ b/.maestro/release_tests/widgets.yaml @@ -8,10 +8,8 @@ appId: com.duckduckgo.mobile.ios --- # Set up -- clearState -- launchApp - runFlow: - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load a website - assertVisible: diff --git a/.maestro/security_tests/1_-_AddressBarSpoof,_basicauth.yaml b/.maestro/security_tests/1_-_AddressBarSpoof,_basicauth.yaml index f5ad473314..3adc46abf8 100644 --- a/.maestro/security_tests/1_-_AddressBarSpoof,_basicauth.yaml +++ b/.maestro/security_tests/1_-_AddressBarSpoof,_basicauth.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml b/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml index 904f5e2508..ebeb17a65e 100644 --- a/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml +++ b/.maestro/security_tests/2_-_AddressBarSpoof,_aboutblank.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/security_tests/3_-_AddressBarSpoof,_appschemes.yaml b/.maestro/security_tests/3_-_AddressBarSpoof,_appschemes.yaml index 9246eb73b8..507ff0c26b 100644 --- a/.maestro/security_tests/3_-_AddressBarSpoof,_appschemes.yaml +++ b/.maestro/security_tests/3_-_AddressBarSpoof,_appschemes.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/security_tests/4_-_AddressBarSpoof,_b64_html.yaml b/.maestro/security_tests/4_-_AddressBarSpoof,_b64_html.yaml index aace88e735..07e7e29052 100644 --- a/.maestro/security_tests/4_-_AddressBarSpoof,_b64_html.yaml +++ b/.maestro/security_tests/4_-_AddressBarSpoof,_b64_html.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/security_tests/5_-_AddressBarSpoof,_downloadpath.yaml b/.maestro/security_tests/5_-_AddressBarSpoof,_downloadpath.yaml index 2b52f2f9fa..4fdfe6e962 100644 --- a/.maestro/security_tests/5_-_AddressBarSpoof,_downloadpath.yaml +++ b/.maestro/security_tests/5_-_AddressBarSpoof,_downloadpath.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/security_tests/6_-_AddressBarSpoof,_formaction.yaml b/.maestro/security_tests/6_-_AddressBarSpoof,_formaction.yaml index aca67548de..7f2e9eab2a 100644 --- a/.maestro/security_tests/6_-_AddressBarSpoof,_formaction.yaml +++ b/.maestro/security_tests/6_-_AddressBarSpoof,_formaction.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/security_tests/7_-_AddressBarSpoof,_pagerewrite.yaml b/.maestro/security_tests/7_-_AddressBarSpoof,_pagerewrite.yaml index e2efd8c291..3021df3a6d 100644 --- a/.maestro/security_tests/7_-_AddressBarSpoof,_pagerewrite.yaml +++ b/.maestro/security_tests/7_-_AddressBarSpoof,_pagerewrite.yaml @@ -2,15 +2,10 @@ appId: com.duckduckgo.mobile.ios tags: - securityTest --- + # Set up -- clearState -- launchApp - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Load Site - assertVisible: diff --git a/.maestro/shared/onboarding.yaml b/.maestro/shared/onboarding.yaml index d624d49521..17bf8ffb35 100644 --- a/.maestro/shared/onboarding.yaml +++ b/.maestro/shared/onboarding.yaml @@ -9,6 +9,8 @@ appId: com.duckduckgo.mobile.ios - tapOn: text: "Let’s Do It!" index: 0 -- assertVisible: "Make DuckDuckGo your default browser." + +# Disabled while UI testing is happening +# - assertVisible: "Make DuckDuckGo your default browser." - tapOn: text: "Skip" diff --git a/.maestro/shared/setup.yaml b/.maestro/shared/setup.yaml new file mode 100644 index 0000000000..4918f67782 --- /dev/null +++ b/.maestro/shared/setup.yaml @@ -0,0 +1,16 @@ +# setup.yaml +appId: com.duckduckgo.mobile.ios + +--- + +# If you need more arguments, copy these two commands directly into your test +# * See release_tests/autoclear.yaml for an example + +- launchApp: + appId: "com.duckduckgo.mobile.ios" + clearState: true + clearKeychain: true + +# Get past onboarding screens +- runFlow: + file: onboarding.yaml diff --git a/.maestro/sync_tests/01_create_account.yaml b/.maestro/sync_tests/01_create_account.yaml index 225b8a7ec5..1176b80efa 100644 --- a/.maestro/sync_tests/01_create_account.yaml +++ b/.maestro/sync_tests/01_create_account.yaml @@ -5,17 +5,9 @@ name: 01_create_account --- -# Clear and launch -- clearState -- launchApp - -# Run onboarding Flow +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Set Internal User - tapOn: Settings diff --git a/.maestro/sync_tests/02_login_account.yaml b/.maestro/sync_tests/02_login_account.yaml index ab524933e8..f0abeba8f7 100644 --- a/.maestro/sync_tests/02_login_account.yaml +++ b/.maestro/sync_tests/02_login_account.yaml @@ -5,17 +5,9 @@ name: 02_login_account --- -# Clear and launch -- clearState -- launchApp - -# Run onboarding Flow +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Copy Recovery Code - tapOn: Settings diff --git a/.maestro/sync_tests/03_recover_account.yaml b/.maestro/sync_tests/03_recover_account.yaml index 5ce0eaa697..2eb94ece2f 100644 --- a/.maestro/sync_tests/03_recover_account.yaml +++ b/.maestro/sync_tests/03_recover_account.yaml @@ -5,17 +5,9 @@ name: 03_recover_account --- -# Clear and launch -- clearState -- launchApp - -# Run onboarding Flow +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Set Internal User - tapOn: Settings diff --git a/.maestro/sync_tests/04_sync_data_setup.yaml b/.maestro/sync_tests/04_sync_data_setup.yaml index 0bdb945862..97ba74b85c 100644 --- a/.maestro/sync_tests/04_sync_data_setup.yaml +++ b/.maestro/sync_tests/04_sync_data_setup.yaml @@ -5,17 +5,9 @@ name: 04_sync_data_setup --- -# Clear and launch -- clearState -- launchApp - -# Run onboarding Flow +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Add local favorite and bookmark - runFlow: diff --git a/.maestro/sync_tests/05_sync_data_check.yaml b/.maestro/sync_tests/05_sync_data_check.yaml index 1716e3637f..87aa410ad7 100644 --- a/.maestro/sync_tests/05_sync_data_check.yaml +++ b/.maestro/sync_tests/05_sync_data_check.yaml @@ -8,17 +8,10 @@ name: 05_sync_data_check # and it will fail if 04 is not executed before. # The test is split in two different flow to accomodate # for Maestro CI max execution time. -# Clear and launch -- clearState -- launchApp -# Run onboarding Flow +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Copy Recovery Code - tapOn: Settings diff --git a/.maestro/sync_tests/06_delete_account.yaml b/.maestro/sync_tests/06_delete_account.yaml index 8321465592..8aa17d3c5c 100644 --- a/.maestro/sync_tests/06_delete_account.yaml +++ b/.maestro/sync_tests/06_delete_account.yaml @@ -4,17 +4,9 @@ tags: name: 06_delete_account --- -# Clear and launch -- clearState -- launchApp - -# Run onboarding Flow +# Set up - runFlow: - when: - visible: - text: "Let’s Do It!" - index: 0 - file: ../shared/onboarding.yaml + file: ../shared/setup.yaml # Set Internal User - tapOn: Settings diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index d2c6604405..093c088c63 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -124,13 +124,13 @@ From 15035df3867098aa74371b4cf041cbb1e16a96de Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 18 Jul 2024 16:02:56 +0200 Subject: [PATCH 35/48] Properly compare actual value of the entitlement check (#3100) Task/Issue URL: https://app.asana.com/0/1203936086921904/1207839252973142/f Description: When building available entitlements list for the settings the entitlement check API should read actual result value instead of only validating if the call succeeded. The entitlement check returns Swift Result type wrapping boolean in the success case. --- DuckDuckGo/SettingsViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index bf1ac1c903..f437dd7d4f 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -727,7 +727,7 @@ extension SettingsViewModel { let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] for entitlement in entitlementsToCheck { - if case .success = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement) { + if case .success(true) = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement) { currentEntitlements.append(entitlement) } } From d9bd26c0c7f55a5de0c7a16256b43f6602a27cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Thu, 18 Jul 2024 16:32:08 +0200 Subject: [PATCH 36/48] New Tab Page favorites section (#3083) --- DuckDuckGo.xcodeproj/project.pbxproj | 60 ++++++- .../DeviceOrientationEnvironmentValue.swift | 49 ++++++ DuckDuckGo/FaviconsHelper.swift | 17 +- DuckDuckGo/Favorite.swift | 56 ++++++ DuckDuckGo/FavoriteIconView.swift | 80 +++++++++ DuckDuckGo/FavoriteItemView.swift | 68 +++++--- DuckDuckGo/FavoritesDefaultModel.swift | 164 ++++++++++++++++++ DuckDuckGo/FavoritesEmptyStateView.swift | 5 +- DuckDuckGo/FavoritesFaviconLoader.swift | 74 ++++++++ DuckDuckGo/FavoritesModel.swift | 37 ++-- DuckDuckGo/FavoritesPreviewModel.swift | 87 ++++++++++ DuckDuckGo/FavoritesView.swift | 59 ++++--- DuckDuckGo/MainViewController.swift | 42 ++++- DuckDuckGo/NewTabPageControllerDelegate.swift | 27 +++ .../NewTabPageCustomizeButtonView.swift | 5 +- DuckDuckGo/NewTabPageGridView.swift | 15 +- DuckDuckGo/NewTabPageView.swift | 88 +++++----- DuckDuckGo/NewTabPageViewController.swift | 70 +++++++- DuckDuckGo/ShortcutItemView.swift | 2 +- DuckDuckGo/ToggleExpandButtonView.swift | 54 +++++- submodules/privacy-reference-tests | 2 +- 21 files changed, 914 insertions(+), 147 deletions(-) create mode 100644 DuckDuckGo/DeviceOrientationEnvironmentValue.swift create mode 100644 DuckDuckGo/Favorite.swift create mode 100644 DuckDuckGo/FavoriteIconView.swift create mode 100644 DuckDuckGo/FavoritesDefaultModel.swift create mode 100644 DuckDuckGo/FavoritesFaviconLoader.swift create mode 100644 DuckDuckGo/FavoritesPreviewModel.swift create mode 100644 DuckDuckGo/NewTabPageControllerDelegate.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 66d3f24b0e..7d8632eaf2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -259,20 +259,27 @@ 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */; }; + 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; }; 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; }; 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; }; 6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; }; + 6FA3438F2C3D3BC300470677 /* Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA3438E2C3D3BC300470677 /* Favorite.swift */; }; + 6FA343922C3D3C3B00470677 /* FavoriteIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */; }; 6FB1FE9E2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */; }; 6FB1FEA22C256ACD0075B68B /* NewTabPageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */; }; 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */; }; 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */; }; 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */; }; - 6FB2A6802C2EA950004D20C8 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesModel.swift */; }; + 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultModel.swift */; }; 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; }; 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */; }; 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */; }; 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; + 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */; }; + 6FD3F8112C3EFCDB00DA5797 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8102C3EFCDB00DA5797 /* FavoritesModel.swift */; }; + 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */; }; + 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */; }; 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; 6FDB3F192BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */; }; 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; @@ -1397,21 +1404,28 @@ 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonView.swift; sourceTree = ""; }; + 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = ""; }; 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = ""; }; 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = ""; }; 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = ""; }; + 6FA3438E2C3D3BC300470677 /* Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favorite.swift; sourceTree = ""; }; + 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteIconView.swift; sourceTree = ""; }; 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsDebugView.swift; sourceTree = ""; }; 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageManager.swift; sourceTree = ""; }; 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteEmptyStateItem.swift; sourceTree = ""; }; 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesEmptyStateView.swift; sourceTree = ""; }; 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageGridView.swift; sourceTree = ""; }; - 6FB2A67F2C2EA950004D20C8 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = ""; }; + 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDefaultModel.swift; sourceTree = ""; }; 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = AdAttribution/AdAttributionPixelReporter.swift; sourceTree = ""; }; 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = AdAttribution/AdAttributionReporterStorage.swift; sourceTree = ""; }; 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; + 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOrientationEnvironmentValue.swift; sourceTree = ""; }; + 6FD3F8102C3EFCDB00DA5797 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = ""; }; + 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewModel.swift; sourceTree = ""; }; + 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDelegate.swift; sourceTree = ""; }; 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteSuggestionsModel.swift; sourceTree = ""; }; 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; @@ -2971,6 +2985,7 @@ EE4BE0082A740BED00CD6AA8 /* ClearTextField.swift */, CB825C912C071B1400BCC586 /* AlertView.swift */, CB825C952C071C9300BCC586 /* AlertViewPresenter.swift */, + 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */, 9FEA222D2C324ECD006B03BF /* ViewVisibility.swift */, 9FEA22282C2E38FA006B03BF /* AnimatableTypingText.swift */, ); @@ -3463,6 +3478,27 @@ name = NewTabPage; sourceTree = ""; }; + 6FA3438D2C3D3BB800470677 /* Model */ = { + isa = PBXGroup; + children = ( + 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultModel.swift */, + 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */, + 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */, + 6FD3F8102C3EFCDB00DA5797 /* FavoritesModel.swift */, + 6FA3438E2C3D3BC300470677 /* Favorite.swift */, + ); + name = Model; + sourceTree = ""; + }; + 6FA343902C3D3C2500470677 /* Item */ = { + isa = PBXGroup; + children = ( + 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */, + 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */, + ); + name = Item; + sourceTree = ""; + }; 6FB1FE9C2C24D4060075B68B /* NewTabPageSectionsDebugView */ = { isa = PBXGroup; children = ( @@ -3494,17 +3530,18 @@ 6FE127362C20436A00EB5724 /* HomeRedesign */ = { isa = PBXGroup; children = ( + 6FE1273B2C204C0D00EB5724 /* Subviews */, 6F03CAF82C32C3AA004179A8 /* Messages */, 6FE127372C20492500EB5724 /* NewTabPage.swift */, 6FE127392C204BD000EB5724 /* NewTabPageView.swift */, 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */, + 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */, 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */, - 6FE1273B2C204C0D00EB5724 /* Views */, ); name = HomeRedesign; sourceTree = ""; }; - 6FE1273B2C204C0D00EB5724 /* Views */ = { + 6FE1273B2C204C0D00EB5724 /* Subviews */ = { isa = PBXGroup; children = ( 6FE127472C20941A00EB5724 /* Shortcuts */, @@ -3513,16 +3550,16 @@ 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */, 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */, ); - name = Views; + name = Subviews; sourceTree = ""; }; 6FE127412C204DE900EB5724 /* Favorites */ = { isa = PBXGroup; children = ( + 6FA343902C3D3C2500470677 /* Item */, + 6FA3438D2C3D3BB800470677 /* Model */, 6FB2A6782C2C5B9E004D20C8 /* EmptyState */, 6FE1273C2C204C2500EB5724 /* FavoritesView.swift */, - 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */, - 6FB2A67F2C2EA950004D20C8 /* FavoritesModel.swift */, ); name = Favorites; sourceTree = ""; @@ -6872,6 +6909,7 @@ 85C11E4C2090888C00BFFEB4 /* HomeRowReminder.swift in Sources */, 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, + 6FD3F8112C3EFCDB00DA5797 /* FavoritesModel.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckNavigationHandling.swift in Sources */, 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, @@ -6955,7 +6993,9 @@ 1DDF40292BA04FCD006850D9 /* SettingsPrivacyProtectionsView.swift in Sources */, F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */, 85F2FFCD2211F615006BB258 /* MainViewController+KeyCommands.swift in Sources */, + 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */, 9FF7E9862C23D10300902BE5 /* BrowsersComparisonChart.swift in Sources */, + 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */, 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */, 858650D9246B0D3C00C36F8A /* DaxOnboardingViewController.swift in Sources */, 312E5746283BB04A00C18FA0 /* AutofillEmptySearchView.swift in Sources */, @@ -6973,7 +7013,7 @@ D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, - 6FB2A6802C2EA950004D20C8 /* FavoritesModel.swift in Sources */, + 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultModel.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -7065,6 +7105,7 @@ 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */, F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */, + 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */, 85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */, D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */, 1EF24235273BB9D200DE3D02 /* IntervalSlider.swift in Sources */, @@ -7080,6 +7121,7 @@ 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, 3712091E2C21E390003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, + 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewModel.swift in Sources */, 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, @@ -7121,11 +7163,13 @@ 984D60B2222A1284003B9E3B /* FeedbackFormViewController.swift in Sources */, 31A42564285A09E800049386 /* FaviconView.swift in Sources */, 85374D3821AC419800FF5A1E /* NavigationSearchHomeViewSectionRenderer.swift in Sources */, + 6FA3438F2C3D3BC300470677 /* Favorite.swift in Sources */, 98E888F2223FCC4A00B608A4 /* OnboardingViewController.swift in Sources */, C1B7B51C28941E980098FD6A /* HomeMessageViewModelBuilder.swift in Sources */, 85BA58551F34F49E00C6E8CA /* AppUserDefaults.swift in Sources */, C1B7B52528941F2A0098FD6A /* RemoteMessagingClient.swift in Sources */, 3132FA2827A0788400DD7A12 /* PassKitPreviewHelper.swift in Sources */, + 6FA343922C3D3C3B00470677 /* FavoriteIconView.swift in Sources */, 8505836C219F424500ED4EDB /* TextFieldWithInsets.swift in Sources */, CBD4F13F279EBFAF00B20FD7 /* HomeMessageViewModel.swift in Sources */, 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */, diff --git a/DuckDuckGo/DeviceOrientationEnvironmentValue.swift b/DuckDuckGo/DeviceOrientationEnvironmentValue.swift new file mode 100644 index 0000000000..406b4da084 --- /dev/null +++ b/DuckDuckGo/DeviceOrientationEnvironmentValue.swift @@ -0,0 +1,49 @@ +// +// DeviceOrientationEnvironmentValue.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +extension EnvironmentValues { + var isLandscapeOrientation: Bool { + self[DeviceOrientationHolderKey.self].deviceOrientation.isLandscape + } + + var deviceOrientation: UIDeviceOrientation { + self[DeviceOrientationHolderKey.self].deviceOrientation + } +} + +private struct DeviceOrientationHolderKey: EnvironmentKey { + static let defaultValue = DeviceOrientationHolder() +} + +private final class DeviceOrientationHolder: ObservableObject { + @Published private(set) var deviceOrientation = UIDevice.current.orientation + + private var observable: NSObjectProtocol? + + init() { + observable = NotificationCenter.default.addObserver( + forName: UIDevice.orientationDidChangeNotification, + object: nil, + queue: .main) { [weak self] _ in + self?.deviceOrientation = UIDevice.current.orientation + } + } +} diff --git a/DuckDuckGo/FaviconsHelper.swift b/DuckDuckGo/FaviconsHelper.swift index 3244cb6f02..c37364fc42 100644 --- a/DuckDuckGo/FaviconsHelper.swift +++ b/DuckDuckGo/FaviconsHelper.swift @@ -91,7 +91,22 @@ struct FaviconsHelper { } } - + + @MainActor + static func loadFaviconSync(forDomain domain: String?, + usingCache cacheType: Favicons.CacheType, + useFakeFavicon: Bool, + preferredFakeFaviconLetters: String? = nil) async -> (image: UIImage?, isFake: Bool) { + await withCheckedContinuation { continuation in + loadFaviconSync(forDomain: domain, + usingCache: cacheType, + useFakeFavicon: useFakeFavicon, + preferredFakeFaviconLetters: preferredFakeFaviconLetters) { image, isFake in + continuation.resume(returning: (image, isFake)) + } + } + } + static func createFakeFavicon(forDomain domain: String, size: CGFloat = 192, backgroundColor: UIColor = UIColor.greyishBrown2, diff --git a/DuckDuckGo/Favorite.swift b/DuckDuckGo/Favorite.swift new file mode 100644 index 0000000000..70262d0086 --- /dev/null +++ b/DuckDuckGo/Favorite.swift @@ -0,0 +1,56 @@ +// +// Favorite.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Bookmarks +import SwiftUI + +struct Favorite: Identifiable, Equatable { + let id: String + let title: String + let domain: String + + let urlObject: URL? + + init(id: String, title: String, domain: String, urlObject: URL? = nil) { + self.id = id + self.title = title + self.domain = domain + self.urlObject = urlObject + } +} + +struct Favicon: Equatable, Hashable { + let image: UIImage + let isUsingBorder: Bool + + static let empty = Self.init(image: UIImage(), isUsingBorder: false) +} + +extension Favorite { + var menuTitle: String { + [title, truncatedUrlString].compactMap { $0 }.joined(separator: "\n") + } + + private var truncatedUrlString: String? { + guard let url = urlObject?.absoluteString else { return nil } + let urlString = url.prefix(100).description + let ellipsis = url.count != urlString.count ? "…" : "" + return urlString + ellipsis + } +} diff --git a/DuckDuckGo/FavoriteIconView.swift b/DuckDuckGo/FavoriteIconView.swift new file mode 100644 index 0000000000..88f4351280 --- /dev/null +++ b/DuckDuckGo/FavoriteIconView.swift @@ -0,0 +1,80 @@ +// +// FavoriteIconView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +protocol FavoritesFaviconLoading { + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon +} + +struct FavoriteIconView: View { + @State private var favicon: Favicon + + let favorite: Favorite + let faviconLoading: FavoritesFaviconLoading? + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(Color(designSystemColor: .surface)) + .shadow(color: .shade(0.12), radius: 0.5, y: 1) + .aspectRatio(1, contentMode: .fit) + + Image(uiImage: favicon.image) + .resizable() + .aspectRatio(1.0, contentMode: .fit) + .if(favicon.isUsingBorder) { + $0.padding(Constant.borderSize) + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .task { + if let favicon = await faviconLoading?.loadFavicon(for: favorite, size: Constant.faviconSize) { + self.favicon = favicon + } + } + } +} + +private struct Constant { + static let faviconSize: CGFloat = 64 + static let borderSize: CGFloat = 12 +} + +#Preview { + VStack(spacing: 8) { + FavoriteIconView(favorite: Favorite.mock("apple.com"), faviconLoading: nil) + FavoriteIconView(favorite: Favorite.mock("duckduckgo.com"), faviconLoading: nil) + FavoriteIconView(favorite: Favorite.mock("foobar.com"), faviconLoading: nil) + } +} + +private extension Favorite { + static func mock(_ domain: String) -> Favorite { + return Favorite(id: domain, title: domain, domain: domain) + } +} + +extension FavoriteIconView { + init(favorite: Favorite, faviconLoading: FavoritesFaviconLoading? = nil) { + let favicon = faviconLoading?.fakeFavicon(for: favorite, size: Constant.faviconSize) ?? .empty + self.init(favicon: favicon, favorite: favorite, faviconLoading: faviconLoading) + } +} diff --git a/DuckDuckGo/FavoriteItemView.swift b/DuckDuckGo/FavoriteItemView.swift index 0780d761f4..8911159ada 100644 --- a/DuckDuckGo/FavoriteItemView.swift +++ b/DuckDuckGo/FavoriteItemView.swift @@ -21,41 +21,63 @@ import DesignResourcesKit import SwiftUI struct FavoriteItemView: View { - let favicon: Image? - let name: String + let favorite: Favorite + let faviconLoading: FavoritesFaviconLoading? + let onMenuAction: ((MenuAction) -> Void)? var body: some View { VStack(spacing: 6) { - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(Color(designSystemColor: .surface)) - .shadow(color: .shade(0.12), radius: 0.5, y: 1) - .aspectRatio(1, contentMode: .fit) - - FavoriteIconView(favicon: favicon) - + FavoriteIconView(favorite: favorite, faviconLoading: faviconLoading) + .contextMenu { + // This context menu can be moved up in the hierarchy to `FavoritesView` once support for iOS 15 is removed. contextMenu with preview modifier can be used then. + contextMenuItems() } - - Text(name) - .daxCaption() + + Text(favorite.title) + .font(Font.system(size: 12)) + .lineLimit(2) + .multilineTextAlignment(.center) .foregroundColor(Color(designSystemColor: .textPrimary)) - .frame(alignment: .center) + .frame(maxWidth: .infinity, alignment: .top) } + .accessibilityElement() + .accessibilityAddTraits(.isButton) + .accessibilityLabel("\(favorite.title). \(UserText.favorite)") } -} -private struct FavoriteIconView: View { - let favicon: Image? + private func contextMenuItems() -> some View { + Section(favorite.menuTitle) { + Button { + onMenuAction?(.edit) + } label: { + Label(UserText.favoriteMenuEdit, image: "Edit") + } - var body: some View { - if let favicon { - favicon - .resizable() - .aspectRatio(1.0, contentMode: .fit) + Button { + onMenuAction?(.delete) + } label: { + Label(UserText.favoriteMenuRemove, image: "RemoveFavoriteMenuIcon") + } } } } +extension FavoriteItemView { + enum MenuAction { + case edit + case delete + } +} + #Preview { - FavoriteItemView(favicon: nil, name: "Text").frame(width: 64, height: 64) + HStack(alignment: .top) { + FavoriteItemView(favorite: Favorite(id: UUID().uuidString, title: "Text", domain: "facebook.com")).frame(width: 64) + FavoriteItemView(favorite: Favorite(id: UUID().uuidString, title: "Lorem Ipsum is simply dummy text of the printing and typesetting industry", domain: "duckduckgo.com")).frame(width: 64) + } +} + +private extension FavoriteItemView { + init(favorite: Favorite) { + self.init(favorite: favorite, faviconLoading: nil, onMenuAction: nil) + } } diff --git a/DuckDuckGo/FavoritesDefaultModel.swift b/DuckDuckGo/FavoritesDefaultModel.swift new file mode 100644 index 0000000000..b91c44c579 --- /dev/null +++ b/DuckDuckGo/FavoritesDefaultModel.swift @@ -0,0 +1,164 @@ +// +// FavoritesDefaultModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Bookmarks +import Combine +import SwiftUI +import Core +import WidgetKit + +final class FavoritesDefaultModel: FavoritesModel { + + @Published private(set) var allFavorites: [Favorite] = [] + @Published private(set) var isCollapsed: Bool = true + + private(set) lazy var faviconLoader: FavoritesFaviconLoading? = { + FavoritesFaviconLoader(onFaviconMissing: { [weak self] in + guard let self else { return } + + await MainActor.run { + self.faviconMissing() + } + }) + }() + + private var cancellables = Set() + + private let interactionModel: FavoritesListInteracting + + var isEmpty: Bool { + allFavorites.isEmpty + } + + init(interactionModel: FavoritesListInteracting) { + self.interactionModel = interactionModel + + interactionModel.externalUpdates.sink { [weak self] _ in + try? self?.updateData() + }.store(in: &cancellables) + + do { + try updateData() + } catch { + fatalError(error.localizedDescription) + } + } + + func toggleCollapse() { + isCollapsed.toggle() + } + + func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + let maxCollapsedItemsCount = columnsCount * 2 + let favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites + let isCollapsible = allFavorites.count > maxCollapsedItemsCount + + return .init(items: favorites, isCollapsible: isCollapsible) + } + + // MARK: - External actions + + var onFaviconMissing: () -> Void = {} + func faviconMissing() { + onFaviconMissing() + } + + var onFavoriteURLSelected: ((URL) -> Void)? + func favoriteSelected(_ favorite: Favorite) { + guard let url = favorite.urlObject else { return } + + Pixel.fire(pixel: .favoriteLaunchedNTP) + DailyPixel.fire(pixel: .favoriteLaunchedNTPDaily) + Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) + + onFavoriteURLSelected?(url) + } + + var onFavoriteDeleted: ((BookmarkEntity) -> Void)? + func deleteFavorite(_ favorite: Favorite) { + guard let entity = lookupEntity(for: favorite) else { return } + + Pixel.fire(pixel: .homeScreenDeleteFavorite) + + interactionModel.removeFavorite(entity) + + WidgetCenter.shared.reloadAllTimelines() + try? updateData() + + onFavoriteDeleted?(entity) + } + + var onFavoriteEdit: ((BookmarkEntity) -> Void)? + func editFavorite(_ favorite: Favorite) { + guard let entity = lookupEntity(for: favorite) else { return } + + Pixel.fire(pixel: .homeScreenEditFavorite) + + onFavoriteEdit?(entity) + } + + private func lookupEntity(for favorite: Favorite) -> BookmarkEntity? { + interactionModel.favorites.first { + $0.uuid == favorite.id + } + } + + private func updateData() throws { + self.allFavorites = try interactionModel.favorites.map(Favorite.init) + } +} + +enum FavoriteMappingError: Error { + case missingUUID +} + +private extension Favorite { + init(_ bookmark: BookmarkEntity) throws { + guard let uuid = bookmark.uuid else { + throw FavoriteMappingError.missingUUID + } + + self.id = uuid + self.title = bookmark.displayTitle + self.domain = bookmark.host + self.urlObject = bookmark.urlObject + } +} + +private extension BookmarkEntity { + + var displayTitle: String { + if let title = title?.trimmingWhitespace(), !title.isEmpty { + return title + } + + if let host = urlObject?.host?.droppingWwwPrefix() { + return host + } + + assertionFailure("Unable to create display title") + return "" + } + + var host: String { + return urlObject?.host ?? "" + } + +} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift index 90af87372c..5e36f1b6cf 100644 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ b/DuckDuckGo/FavoritesEmptyStateView.swift @@ -21,7 +21,8 @@ import SwiftUI struct FavoritesEmptyStateView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass - + @Environment(\.isLandscapeOrientation) var isLandscape + @State var headerPadding: CGFloat = 10 var body: some View { @@ -41,7 +42,7 @@ struct FavoritesEmptyStateView: View { }) ) .onPreferenceChange(WidthKey.self, perform: { fullWidth in - let columnsCount = Double(NewTabPageGrid.columnsCount(for: horizontalSizeClass)) + let columnsCount = Double(NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape)) let allColumnsWidth = columnsCount * NewTabPageGrid.Item.edgeSize let leftoverWidth = fullWidth - allColumnsWidth let spacingSize = leftoverWidth / (columnsCount) diff --git a/DuckDuckGo/FavoritesFaviconLoader.swift b/DuckDuckGo/FavoritesFaviconLoader.swift new file mode 100644 index 0000000000..874ff59421 --- /dev/null +++ b/DuckDuckGo/FavoritesFaviconLoader.swift @@ -0,0 +1,74 @@ +// +// FavoritesFaviconLoader.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +actor FavoritesFaviconLoader: FavoritesFaviconLoading { + private var tasks: [URL: Task] = [:] + private(set) var onFaviconMissing: (() async -> Void)? + + init(onFaviconMissing: (() async -> Void)? = nil) { + self.onFaviconMissing = onFaviconMissing + } + + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + guard let url = favorite.urlObject else { return nil } + + if let task = tasks[url] { + if task.isCancelled { + tasks.removeValue(forKey: url) + } else { + return await task.value + } + } + + let newTask = Task { + let faviconResult = await FaviconsHelper.loadFaviconSync(forDomain: favorite.domain, usingCache: .fireproof, useFakeFavicon: false) + if let iconImage = faviconResult.image { + let useBorder = URL.isDuckDuckGo(domain: favorite.domain) || iconImage.size.width < size + + return Favicon(image: iconImage, isUsingBorder: useBorder) + } else { + await onFaviconMissing?() + return nil + } + } + + tasks[url] = newTask + + return await newTask.value + } + + nonisolated func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + let domain = favorite.domain + let color = UIColor.forDomain(domain) + let icon = FaviconsHelper.createFakeFavicon( + forDomain: domain, + size: 64, + backgroundColor: color, + bold: false + ) + + if let icon { + return Favicon(image: icon, isUsingBorder: false) + } else { + return .empty + } + } +} diff --git a/DuckDuckGo/FavoritesModel.swift b/DuckDuckGo/FavoritesModel.swift index b1e8a494ca..60313089b1 100644 --- a/DuckDuckGo/FavoritesModel.swift +++ b/DuckDuckGo/FavoritesModel.swift @@ -19,26 +19,27 @@ import Foundation -struct Favorite: Identifiable, Equatable { - let id: Int -} +protocol FavoritesModel: AnyObject, ObservableObject { + var allFavorites: [Favorite] { get } + var faviconLoader: FavoritesFaviconLoading? { get } + + var isEmpty: Bool { get } + var isCollapsed: Bool { get } + + func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice -final class FavoritesModel: ObservableObject { + func faviconMissing() - @Published private(set) var allFavorites: [Favorite] - var isEmpty: Bool { - allFavorites.isEmpty - } + // MARK: - Interactions - init() { - self.allFavorites = [] - } + func toggleCollapse() + + func favoriteSelected(_ favorite: Favorite) + func editFavorite(_ favorite: Favorite) + func deleteFavorite(_ favorite: Favorite) +} - func toggleFavoritesPresence() { - if isEmpty { - allFavorites = (1...50).map { Favorite(id: $0) } - } else { - allFavorites = [] - } - } +struct FavoritesSlice { + let items: [Favorite] + let isCollapsible: Bool } diff --git a/DuckDuckGo/FavoritesPreviewModel.swift b/DuckDuckGo/FavoritesPreviewModel.swift new file mode 100644 index 0000000000..a3e145013f --- /dev/null +++ b/DuckDuckGo/FavoritesPreviewModel.swift @@ -0,0 +1,87 @@ +// +// FavoritesPreviewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Bookmarks +import Foundation + +final class FavoritesPreviewModel: FavoritesModel { + var isCollapsed: Bool = true + + @Published var allFavorites: [Favorite] + + var isEmpty: Bool { allFavorites.isEmpty } + var faviconLoader: FavoritesFaviconLoading? { nil } + + init(allFavorites: [Favorite]) { + self.allFavorites = allFavorites + } + + convenience init() { + let favorites = (0...20).map { + Favorite( + id: UUID().uuidString, + title: "Favorite \($0)", + domain: "favorite\($0).domain.com") + } + + self.init(allFavorites: favorites) + } + + func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + let maxCollapsedItemsCount = columnsCount * 2 + let favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites + let isCollapsible = allFavorites.count > maxCollapsedItemsCount + + return .init(items: favorites, isCollapsible: isCollapsible) + } + + func toggleCollapse() { + isCollapsed.toggle() + } + + func faviconMissing() { + + } + + func favoriteSelected(_ favorite: Favorite) { + + } + + func deleteFavorite(_ favorite: Favorite) { + + } + + func editFavorite(_ favorite: Favorite) { + + } + + func loadFavicon(for favorite: Favorite, size: CGFloat) async { + + } +} + +struct EmptyFaviconLoading: FavoritesFaviconLoading { + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + .empty + } + + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + nil + } +} diff --git a/DuckDuckGo/FavoritesView.swift b/DuckDuckGo/FavoritesView.swift index cdb66ae363..24a01a2bc0 100644 --- a/DuckDuckGo/FavoritesView.swift +++ b/DuckDuckGo/FavoritesView.swift @@ -17,42 +17,59 @@ // limitations under the License. // -import Common -import DesignResourcesKit -import DuckUI +import Bookmarks import SwiftUI -struct FavoritesView: View { +struct FavoritesView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject var model: FavoritesModel - - @State var isCollapsed: Bool = true - + @Environment(\.isLandscapeOrientation) var isLandscape + + @ObservedObject var model: Model + + private let selectionFeedback = UISelectionFeedbackGenerator() + var body: some View { - VStack(alignment: .center) { - - let collapsedMaxItemsCount = NewTabPageGrid.columnsCount(for: horizontalSizeClass) * 2 - - let data = isCollapsed ? Array(model.allFavorites.prefix(collapsedMaxItemsCount)) : model.allFavorites - + VStack(alignment: .center, spacing: 24) { + + let columns = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape) + let result = model.prefixedFavorites(for: columns) + NewTabPageGridView { _ in - ForEach(data) { item in - FavoriteItemView(favicon: nil, name: "\(item.id)") + ForEach(result.items) { item in + Button(action: { + model.favoriteSelected(item) + selectionFeedback.selectionChanged() + }, label: { + FavoriteItemView( + favorite: item, + faviconLoading: model.faviconLoader, + onMenuAction: { action in + switch action { + case .delete: model.deleteFavorite(item) + case .edit: model.editFavorite(item) + } + }) + .background(.clear) .frame(width: NewTabPageGrid.Item.edgeSize) + }) } } - - if model.allFavorites.count > collapsedMaxItemsCount { + + if result.isCollapsible { Button(action: { - isCollapsed.toggle() + withAnimation(.easeInOut) { + model.toggleCollapse() + } }, label: { - ToggleExpandButtonView(isIndicatingExpand: isCollapsed).padding() + Image(model.isCollapsed ? .chevronDown : .chevronUp) + .resizable() }) + .buttonStyle(ToggleExpandButtonStyle()) } } } } #Preview { - FavoritesView(model: FavoritesModel()) + FavoritesView(model: FavoritesPreviewModel()) } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 932278163c..796a671420 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -736,7 +736,13 @@ class MainViewController: UIViewController { } if homeTabManager.isNewTabPageSectionsEnabled { - let controller = NewTabPageViewController(homePageMessagesConfiguration: homePageConfiguration) + let controller = NewTabPageViewController(interactionModel: favoritesViewModel, + syncService: syncService, + syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, + homePageMessagesConfiguration: homePageConfiguration) + + controller.delegate = self + newTabPageViewController = controller addToContentContainer(controller: controller) viewCoordinator.logoContainer.isHidden = true @@ -1955,6 +1961,18 @@ extension MainViewController: AutocompleteViewControllerDelegate { } +extension MainViewController { + private func handleRequestedURL(_ url: URL) { + showKeyboardAfterFireButton?.cancel() + + if url.isBookmarklet() { + executeBookmarklet(url) + } else { + loadUrl(url) + } + } +} + extension MainViewController: HomeControllerDelegate { func home(_ home: HomeViewController, didRequestQuery query: String) { @@ -1962,13 +1980,7 @@ extension MainViewController: HomeControllerDelegate { } func home(_ home: HomeViewController, didRequestUrl url: URL) { - showKeyboardAfterFireButton?.cancel() - - if url.isBookmarklet() { - executeBookmarklet(url) - } else { - loadUrl(url) - } + handleRequestedURL(url) } func home(_ home: HomeViewController, didRequestEdit favorite: BookmarkEntity) { @@ -2004,6 +2016,20 @@ extension MainViewController: HomeControllerDelegate { } +extension MainViewController: NewTabPageControllerDelegate { + func newTabPageDidOpenFavoriteURL(_ controller: NewTabPageViewController, url: URL) { + handleRequestedURL(url) + } + + func newTabPageDidEditFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) { + segueToEditBookmark(favorite) + } + + func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) { + // no-op for now + } +} + extension MainViewController: TabDelegate { func tab(_ tab: TabViewController, diff --git a/DuckDuckGo/NewTabPageControllerDelegate.swift b/DuckDuckGo/NewTabPageControllerDelegate.swift new file mode 100644 index 0000000000..15981c4b5d --- /dev/null +++ b/DuckDuckGo/NewTabPageControllerDelegate.swift @@ -0,0 +1,27 @@ +// +// NewTabPageControllerDelegate.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Bookmarks +import Foundation + +protocol NewTabPageControllerDelegate: AnyObject { + func newTabPageDidOpenFavoriteURL(_ controller: NewTabPageViewController, url: URL) + func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) + func newTabPageDidEditFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) +} diff --git a/DuckDuckGo/NewTabPageCustomizeButtonView.swift b/DuckDuckGo/NewTabPageCustomizeButtonView.swift index 2d74483a27..a0c0332ded 100644 --- a/DuckDuckGo/NewTabPageCustomizeButtonView.swift +++ b/DuckDuckGo/NewTabPageCustomizeButtonView.swift @@ -22,10 +22,7 @@ import DuckUI struct NewTabPageCustomizeButtonView: View { var body: some View { - HStack { - Image(.options16) - Text("Customize") - } + Image(.options16) } } diff --git a/DuckDuckGo/NewTabPageGridView.swift b/DuckDuckGo/NewTabPageGridView.swift index 6a01cf2393..9f87036355 100644 --- a/DuckDuckGo/NewTabPageGridView.swift +++ b/DuckDuckGo/NewTabPageGridView.swift @@ -21,22 +21,22 @@ import SwiftUI struct NewTabPageGridView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass - + @Environment(\.isLandscapeOrientation) var isLandscape + @ViewBuilder var content: (_ columnsCount: Int) -> Content var body: some View { - let columnsCount = NewTabPageGrid.columnsCount(for: horizontalSizeClass) + let columnsCount = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape) - LazyVGrid(columns: flexibleColumns(columnsCount), content: { + LazyVGrid(columns: flexibleColumns(columnsCount), spacing: 24, content: { content(columnsCount) }) .padding(0) .offset(.zero) - .clipped() } private func flexibleColumns(_ count: Int) -> [GridItem] { - Array(repeating: GridItem(.flexible(minimum: NewTabPageGrid.Item.edgeSize)), count: count) + Array(repeating: GridItem(.flexible(minimum: NewTabPageGrid.Item.edgeSize), alignment: .top), count: count) } } @@ -50,7 +50,8 @@ enum NewTabPageGrid { static let edgeSize = 64.0 } - static func columnsCount(for sizeClass: UserInterfaceSizeClass?) -> Int { - sizeClass == .regular ? ColumnCount.regular : ColumnCount.compact + static func columnsCount(for sizeClass: UserInterfaceSizeClass?, isLandscape: Bool) -> Int { + let usesWideLayout = isLandscape || sizeClass == .regular + return usesWideLayout ? ColumnCount.regular : ColumnCount.compact } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index e0bb708ab8..95298a90cd 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -21,13 +21,13 @@ import SwiftUI import DuckUI import RemoteMessaging -struct NewTabPageView: View { +struct NewTabPageView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject var messagesModel: NewTabPageMessagesModel - @ObservedObject var favoritesModel: FavoritesModel + @ObservedObject var favoritesModel: FavoritesModelType - init(messagesModel: NewTabPageMessagesModel, favoritesModel: FavoritesModel) { + init(messagesModel: NewTabPageMessagesModel, favoritesModel: FavoritesModelType) { self.messagesModel = messagesModel self.favoritesModel = favoritesModel @@ -35,47 +35,57 @@ struct NewTabPageView: View { } var body: some View { - ScrollView { - VStack { - // MARK: Messages - ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in - HomeMessageView(viewModel: messageModel) - .frame(maxWidth: horizontalSizeClass == .regular ? Constant.messageMaximumWidthPad : Constant.messageMaximumWidth) - .padding(16) - } - - // MARK: Favorites - if favoritesModel.isEmpty { - FavoritesEmptyStateView() + GeometryReader { proxy in + ScrollView { + VStack { + // MARK: Messages + ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in + HomeMessageView(viewModel: messageModel) + .frame(maxWidth: horizontalSizeClass == .regular ? Constant.messageMaximumWidthPad : Constant.messageMaximumWidth) + .padding(16) + } + + // MARK: Favorites + if favoritesModel.isEmpty { + FavoritesEmptyStateView() + .padding(Constant.sectionPadding) + } else { + FavoritesView(model: favoritesModel) + .padding(Constant.sectionPadding) + } + + // MARK: Shortcuts + ShortcutsView() .padding(Constant.sectionPadding) - } else { - FavoritesView(model: favoritesModel) - .padding(Constant.sectionPadding) - } - // MARK: Shortcuts - ShortcutsView() - .padding(Constant.sectionPadding) - - // MARK: Customize - Button(action: { - // Temporary action for testing purposes - favoritesModel.toggleFavoritesPresence() - }, label: { - NewTabPageCustomizeButtonView() - }).buttonStyle(SecondaryFillButtonStyle(compact: true, fullWidth: false)) - .padding(EdgeInsets(top: 88, leading: 0, bottom: 16, trailing: 0)) + Spacer() + + // MARK: Customize button + HStack { + Spacer() + + Button(action: { + }, label: { + NewTabPageCustomizeButtonView() + // Needed to reduce default button margins + .padding(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) + }).buttonStyle(SecondaryFillButtonStyle(compact: true, fullWidth: false)) + .padding(Constant.sectionPadding) + .padding(.top, 40) + } + } + .frame(minHeight: proxy.frame(in: .local).size.height) } + .background(Color(designSystemColor: .background)) } - .background(Color(designSystemColor: .background)) } +} - private struct Constant { - static let sectionPadding = EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) +private struct Constant { + static let sectionPadding = EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) - static let messageMaximumWidth: CGFloat = 380 - static let messageMaximumWidthPad: CGFloat = 455 - } + static let messageMaximumWidth: CGFloat = 380 + static let messageMaximumWidthPad: CGFloat = 455 } // MARK: - Preview @@ -87,7 +97,7 @@ struct NewTabPageView: View { homeMessages: [] ) ), - favoritesModel: FavoritesModel() + favoritesModel: FavoritesPreviewModel() ) } @@ -107,7 +117,7 @@ struct NewTabPageView: View { ] ) ), - favoritesModel: FavoritesModel() + favoritesModel: FavoritesPreviewModel() ) } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index f8f66c5113..6b187d8660 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -18,24 +18,69 @@ // import SwiftUI +import DDGSync +import Bookmarks +import Core -final class NewTabPageViewController: UIHostingController, NewTabPage { +final class NewTabPageViewController: UIHostingController>, NewTabPage { - init(homePageMessagesConfiguration: HomePageMessagesConfiguration) { + private let syncService: DDGSyncing + private let syncBookmarksAdapter: SyncBookmarksAdapter + + private(set) lazy var faviconsFetcherOnboarding = FaviconsFetcherOnboarding(syncService: syncService, syncBookmarksAdapter: syncBookmarksAdapter) + + private let favoritesModel: FavoritesDefaultModel + + init(interactionModel: FavoritesListInteracting, + syncService: DDGSyncing, + syncBookmarksAdapter: SyncBookmarksAdapter, + homePageMessagesConfiguration: HomePageMessagesConfiguration) { + + self.syncService = syncService + self.syncBookmarksAdapter = syncBookmarksAdapter + + self.favoritesModel = FavoritesDefaultModel(interactionModel: interactionModel) let newTabPageView = NewTabPageView(messagesModel: NewTabPageMessagesModel(homePageMessagesConfiguration: homePageMessagesConfiguration), - favoritesModel: FavoritesModel()) + favoritesModel: favoritesModel) + super.init(rootView: newTabPageView) + + assignFavoriteModelActions() } - - @available(*, unavailable) - @MainActor required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + + // MARK: - Private + + private func assignFavoriteModelActions() { + favoritesModel.onFaviconMissing = { [weak self] in + guard let self else { return } + self.faviconsFetcherOnboarding.presentOnboardingIfNeeded(from: self) + } + + favoritesModel.onFavoriteURLSelected = { [weak self] url in + guard let self else { return } + + delegate?.newTabPageDidOpenFavoriteURL(self, url: url) + } + + favoritesModel.onFavoriteEdit = { [weak self] favorite in + guard let self else { return } + + delegate?.newTabPageDidEditFavorite(self, favorite: favorite) + } + + favoritesModel.onFavoriteDeleted = { [weak self] favorite in + guard let self else { return } + + delegate?.newTabPageDidDeleteFavorite(self, favorite: favorite) + } } - + + // MARK: - NewTabPage + let isDragging: Bool = false weak var chromeDelegate: BrowserChromeDelegate? - weak var delegate: HomeControllerDelegate? + weak var delegate: NewTabPageControllerDelegate? func launchNewSearch() { @@ -64,4 +109,11 @@ final class NewTabPageViewController: UIHostingController, NewTa func reloadFavorites() { } + + // MARK: - + + @available(*, unavailable) + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } diff --git a/DuckDuckGo/ShortcutItemView.swift b/DuckDuckGo/ShortcutItemView.swift index 914ba7b202..0a7d8a4f2a 100644 --- a/DuckDuckGo/ShortcutItemView.swift +++ b/DuckDuckGo/ShortcutItemView.swift @@ -37,7 +37,7 @@ struct ShortcutItemView: View { .frame(width: NewTabPageGrid.Item.edgeSize * 0.5) } Text(name) - .daxCaption() + .font(Font.system(size: 12)) .foregroundColor(Color(designSystemColor: .textPrimary)) .frame(alignment: .center) } diff --git a/DuckDuckGo/ToggleExpandButtonView.swift b/DuckDuckGo/ToggleExpandButtonView.swift index 376a1e52b1..eb4a9ac695 100644 --- a/DuckDuckGo/ToggleExpandButtonView.swift +++ b/DuckDuckGo/ToggleExpandButtonView.swift @@ -20,16 +20,60 @@ import SwiftUI import DuckUI -struct ToggleExpandButtonView: View { +struct ToggleExpandButtonStyle: ButtonStyle { + @Environment(\.colorScheme) private var colorScheme - let isIndicatingExpand: Bool + func makeBody(configuration: Configuration) -> some View { + let isDark = colorScheme == .dark + HStack(spacing: 0) { + VStack { + ExpandButtonDivider() + } + ZStack { + Circle() + .stroke(Color(designSystemColor: .lines), lineWidth: 1) + .frame(width: 32) + .if(configuration.isPressed, transform: { + $0.background(Circle() + .fill(isDark ? Color.tint(0.12) : Color.shade(0.06))) + }) + .background( + Circle() + .fill(Color(designSystemColor: .background)) + ) + configuration.label + .foregroundColor(isDark ? .tint(0.6) : .shade(0.6)) + .frame(width: 16, height: 16) + } + VStack { + ExpandButtonDivider() + } + } + } +} + +private struct ExpandButtonDivider: View { var body: some View { - Text(isIndicatingExpand ? "Show more" : "Show less") - .daxCaption() + Rectangle() + .frame(maxWidth: .infinity) + .frame(height: 1) + .foregroundColor(Color(designSystemColor: .lines)) } } #Preview { - return ToggleExpandButtonView(isIndicatingExpand: true) + VStack { + Button(action: {}, + label: { + Image(.chevronDown) + .resizable() + }).buttonStyle(ToggleExpandButtonStyle()) + + Button(action: {}, + label: { + Image(.chevronUp) + .resizable() + }).buttonStyle(ToggleExpandButtonStyle()) + } } diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index a242bf03ff..a603ff9af2 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit a242bf03ff33b573eb716405b15924cc712d41c1 +Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8 From 4d72d384458cbf1b34d2ed72ec19df2cc6f47a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Thu, 18 Jul 2024 16:57:20 +0200 Subject: [PATCH 37/48] Update Package.resolved file (#3102) --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ca5f14f5c0..52187eba69 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "09844ec2d0c9d2312e02c90527e8df063db89318", - "version" : "171.2.1" + "revision" : "0e7f13b24876a28934320ebf0ec14f644a211869", + "version" : "171.2.2" } }, { From e85cc8c0fe378d5fa35e13e80ba69ed5184c53e0 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jul 2024 17:08:13 +0200 Subject: [PATCH 38/48] Remove print (#3101) Task/Issue URL: https://app.asana.com/0/414235014887631/1207845393376349/f Description: Remove a print statement --- DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift index d28415eec7..d5288d97cd 100644 --- a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift @@ -25,11 +25,7 @@ import Core final class YoutubePlayerNavigationHandler { var duckPlayer: DuckPlayerProtocol - var referrer: DuckPlayerReferrer = .other { - didSet { - print(referrer) - } - } + var referrer: DuckPlayerReferrer = .other private struct Constants { static let SERPURL = "https://duckduckgo.com/" From 5f7c4cb4aa46f3bd592147f6814dcfb86b78cf10 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 18 Jul 2024 18:40:07 +0200 Subject: [PATCH 39/48] Updates BSK to 171.2.3 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7d8632eaf2..bdc66796da 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10166,7 +10166,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 171.2.2; + version = 171.2.3; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 52187eba69..e22a6b3a86 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "0e7f13b24876a28934320ebf0ec14f644a211869", - "version" : "171.2.2" + "revision" : "278f486e71131ee8e36df4180518b0f74843d47e", + "version" : "171.2.3" } }, { From 6885becaac72129462f5d2be5469e23ba04ccec7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:35:40 +0200 Subject: [PATCH 40/48] Bump submodules/privacy-reference-tests from `a242bf0` to `afb4f61` (#3096) Signed-off-by: dependabot[bot] --- submodules/privacy-reference-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index a603ff9af2..afb4f6128a 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8 +Subproject commit afb4f6128a3b50d53ddcb1897ea1fb4df6858aa1 From 860ec2e265e6c9a598e6dd9067db49eaabf11f06 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 19 Jul 2024 08:43:52 +0200 Subject: [PATCH 41/48] Add support for skipping sending usage pixels for remote messages (#3106) Task/Issue URL: https://app.asana.com/0/1201621708115095/1207841204698435/f Description: This change allows to skip sending usage pixels for a given remote message if that's stated in the RMF config. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/Debug.storyboard | 64 +++++++--- DuckDuckGo/HistoryDebugViewController.swift | 6 +- DuckDuckGo/HomeMessageView.swift | 5 + DuckDuckGo/HomeMessageViewModel.swift | 1 + DuckDuckGo/HomeMessageViewModelBuilder.swift | 8 +- .../HomeMessageViewSectionRenderer.swift | 26 ++-- DuckDuckGo/HomePageConfiguration.swift | 12 +- DuckDuckGo/NewTabPageMessagesModel.swift | 26 ++-- DuckDuckGo/NewTabPageView.swift | 3 +- .../RemoteMessagingDebugViewController.swift | 113 ++++++++++++++++++ .../NewTabPageMessagesModelTests.swift | 69 ++++++++++- 13 files changed, 292 insertions(+), 51 deletions(-) create mode 100644 DuckDuckGo/RemoteMessagingDebugViewController.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bdc66796da..a4dd4fccca 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -188,6 +188,7 @@ 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; 37A6A8FE2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8FD2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift */; }; + 37C696772C4957940073E131 /* RemoteMessagingDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C696762C4957940073E131 /* RemoteMessagingDebugViewController.swift */; }; 37CBCA9E2A8A659C0050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA9D2A8A659C0050218F /* SyncSettingsAdapter.swift */; }; 37CEFCAC2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEFCAB2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift */; }; 37CF91602BB4737300BADCAE /* CrashCollectionOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CF915F2BB4737300BADCAE /* CrashCollectionOnboarding.swift */; }; @@ -1342,6 +1343,7 @@ 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeSyncHandler.swift; sourceTree = ""; }; 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCleanupErrorHandling.swift; sourceTree = ""; }; 37A6A8FD2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboarding.swift; sourceTree = ""; }; + 37C696762C4957940073E131 /* RemoteMessagingDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingDebugViewController.swift; sourceTree = ""; }; 37CBCA9D2A8A659C0050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CEFCAB2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsCleanupErrorHandling.swift; sourceTree = ""; }; 37CF915F2BB4737300BADCAE /* CrashCollectionOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashCollectionOnboarding.swift; sourceTree = ""; }; @@ -3976,6 +3978,7 @@ CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */, 851624C62B96389D002D5CD7 /* HistoryDebugViewController.swift */, 98A860EE2C4682E00077FE4D /* BookmarksDebugViewController.swift */, + 37C696762C4957940073E131 /* RemoteMessagingDebugViewController.swift */, ); name = Debug; sourceTree = ""; @@ -7146,6 +7149,7 @@ 6FB1FEA22C256ACD0075B68B /* NewTabPageManager.swift in Sources */, 9865DFF922A8220D00D27829 /* FavoritesOverlay.swift in Sources */, 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */, + 37C696772C4957940073E131 /* RemoteMessagingDebugViewController.swift in Sources */, F4F6DFB626E6B71300ED7E12 /* BookmarkFoldersTableViewController.swift in Sources */, 8586A11024CCCD040049720E /* TabsBarViewController.swift in Sources */, F1D796F41E7C2A410019D451 /* BookmarksDelegate.swift in Sources */, @@ -10166,7 +10170,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 171.2.3; + version = 172.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e22a6b3a86..aea2579038 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "278f486e71131ee8e36df4180518b0f74843d47e", - "version" : "171.2.3" + "revision" : "f8e381771a33287e317da84d5676a5e2a271bca3", + "version" : "172.0.0" } }, { diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index 2ae12b72f9..46d581edff 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -257,9 +257,21 @@ - + + + + + + + + + + + + + @@ -267,7 +279,7 @@ - + @@ -276,7 +288,7 @@ - + @@ -285,7 +297,7 @@ - + @@ -294,7 +306,7 @@ - + @@ -303,7 +315,7 @@ - + @@ -312,7 +324,7 @@ - + @@ -321,7 +333,7 @@ - + @@ -330,7 +342,7 @@ - + @@ -339,7 +351,7 @@ - + @@ -386,6 +398,22 @@ + + + + + + + + + + + + + + + + @@ -895,34 +923,34 @@ - + - + - + - + diff --git a/DuckDuckGo/HistoryDebugViewController.swift b/DuckDuckGo/HistoryDebugViewController.swift index 3d516818c3..a92a5b1dac 100644 --- a/DuckDuckGo/HistoryDebugViewController.swift +++ b/DuckDuckGo/HistoryDebugViewController.swift @@ -49,10 +49,8 @@ struct HistoryDebugRootView: View { } .navigationTitle("\(model.history.count) History Items") .toolbar { - if #available(iOS 15, *) { - Button("Delete All", role: .destructive) { - model.deleteAll() - } + Button("Delete All", role: .destructive) { + model.deleteAll() } } } diff --git a/DuckDuckGo/HomeMessageView.swift b/DuckDuckGo/HomeMessageView.swift index 3eacf7696d..5c6834306e 100644 --- a/DuckDuckGo/HomeMessageView.swift +++ b/DuckDuckGo/HomeMessageView.swift @@ -327,22 +327,27 @@ struct HomeMessageView_Previews: PreviewProvider { static var previews: some View { Group { HomeMessageView(viewModel: HomeMessageViewModel(messageId: "Small", + sendPixels: false, modelType: small, onDidClose: { _ in }, onDidAppear: {})) HomeMessageView(viewModel: HomeMessageViewModel(messageId: "Critical", + sendPixels: false, modelType: critical, onDidClose: { _ in }, onDidAppear: {})) HomeMessageView(viewModel: HomeMessageViewModel(messageId: "Big Single", + sendPixels: false, modelType: bigSingle, onDidClose: { _ in }, onDidAppear: {})) HomeMessageView(viewModel: HomeMessageViewModel(messageId: "Big Two", + sendPixels: false, modelType: bigTwo, onDidClose: { _ in }, onDidAppear: {})) HomeMessageView(viewModel: HomeMessageViewModel(messageId: "Promo", + sendPixels: false, modelType: promo, onDidClose: { _ in }, onDidAppear: {})) } diff --git a/DuckDuckGo/HomeMessageViewModel.swift b/DuckDuckGo/HomeMessageViewModel.swift index e6c7bbdd0c..26ddd24d83 100644 --- a/DuckDuckGo/HomeMessageViewModel.swift +++ b/DuckDuckGo/HomeMessageViewModel.swift @@ -31,6 +31,7 @@ struct HomeMessageViewModel { } let messageId: String + let sendPixels: Bool let modelType: RemoteMessageModelType var image: String? { diff --git a/DuckDuckGo/HomeMessageViewModelBuilder.swift b/DuckDuckGo/HomeMessageViewModelBuilder.swift index 641be47a10..42dd27f6b0 100644 --- a/DuckDuckGo/HomeMessageViewModelBuilder.swift +++ b/DuckDuckGo/HomeMessageViewModelBuilder.swift @@ -37,7 +37,13 @@ struct HomeMessageViewModelBuilder { onDidAppear: @escaping () -> Void) -> HomeMessageViewModel? { guard let content = remoteMessage.content else { return nil } - return HomeMessageViewModel(messageId: remoteMessage.id, modelType: content, onDidClose: onDidClose, onDidAppear: onDidAppear) + return HomeMessageViewModel( + messageId: remoteMessage.id, + sendPixels: remoteMessage.isMetricsEnabled, + modelType: content, + onDidClose: onDidClose, + onDidAppear: onDidAppear + ) } } diff --git a/DuckDuckGo/HomeMessageViewSectionRenderer.swift b/DuckDuckGo/HomeMessageViewSectionRenderer.swift index 8355f2466a..e039c10062 100644 --- a/DuckDuckGo/HomeMessageViewSectionRenderer.swift +++ b/DuckDuckGo/HomeMessageViewSectionRenderer.swift @@ -109,7 +109,7 @@ class HomeMessageViewSectionRenderer: NSObject, HomeViewSectionRenderer { let message = homePageConfiguration.homeMessages[indexPath.row] switch message { case .placeholder: - return HomeMessageViewModel(messageId: "", modelType: .small(titleText: "", descriptionText: "")) { [weak self] _ in + return HomeMessageViewModel(messageId: "", sendPixels: false, modelType: .small(titleText: "", descriptionText: "")) { [weak self] _ in self?.dismissHomeMessage(message, at: indexPath, in: collectionView) } onDidAppear: { // no-op @@ -126,27 +126,35 @@ class HomeMessageViewSectionRenderer: NSObject, HomeViewSectionRenderer { if !isSharing { self.dismissHomeMessage(message, at: indexPath, in: collectionView) } - Pixel.fire(pixel: .remoteMessageActionClicked, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + Pixel.fire(pixel: .remoteMessageActionClicked, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } case .primaryAction(let isSharing): if !isSharing { self.dismissHomeMessage(message, at: indexPath, in: collectionView) } - Pixel.fire(pixel: .remoteMessagePrimaryActionClicked, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + Pixel.fire(pixel: .remoteMessagePrimaryActionClicked, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } case .secondaryAction(let isSharing): if !isSharing { self.dismissHomeMessage(message, at: indexPath, in: collectionView) } - Pixel.fire(pixel: .remoteMessageSecondaryActionClicked, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + Pixel.fire(pixel: .remoteMessageSecondaryActionClicked, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } case .close: self.dismissHomeMessage(message, at: indexPath, in: collectionView) - Pixel.fire(pixel: .remoteMessageDismissed, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + Pixel.fire(pixel: .remoteMessageDismissed, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } } } onDidAppear: { [weak self] in diff --git a/DuckDuckGo/HomePageConfiguration.swift b/DuckDuckGo/HomePageConfiguration.swift index 14aa2c37d1..ed9da38ae2 100644 --- a/DuckDuckGo/HomePageConfiguration.swift +++ b/DuckDuckGo/HomePageConfiguration.swift @@ -98,13 +98,17 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { switch homeMessage { case .remoteMessage(let remoteMessage): os_log("Remote message shown: %s", log: .remoteMessaging, type: .info, remoteMessage.id) - Pixel.fire(pixel: .remoteMessageShown, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + Pixel.fire(pixel: .remoteMessageShown, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } if !remoteMessagingClient.store.hasShownRemoteMessage(withID: remoteMessage.id) { os_log("Remote message shown for first time: %s", log: .remoteMessaging, type: .info, remoteMessage.id) - Pixel.fire(pixel: .remoteMessageShownUnique, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + Pixel.fire(pixel: .remoteMessageShownUnique, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } remoteMessagingClient.store.updateRemoteMessage(withID: remoteMessage.id, asShown: true) } diff --git a/DuckDuckGo/NewTabPageMessagesModel.swift b/DuckDuckGo/NewTabPageMessagesModel.swift index 486790bd58..996d6b7ea2 100644 --- a/DuckDuckGo/NewTabPageMessagesModel.swift +++ b/DuckDuckGo/NewTabPageMessagesModel.swift @@ -73,7 +73,7 @@ final class NewTabPageMessagesModel: ObservableObject { private func homeMessageViewModel(for message: HomeMessage) -> HomeMessageViewModel? { switch message { case .placeholder: - return HomeMessageViewModel(messageId: "", modelType: .small(titleText: "", descriptionText: "")) { [weak self] _ in + return HomeMessageViewModel(messageId: "", sendPixels: false, modelType: .small(titleText: "", descriptionText: "")) { [weak self] _ in self?.dismissHomeMessage(message) } onDidAppear: { // no-op @@ -90,27 +90,35 @@ final class NewTabPageMessagesModel: ObservableObject { if !isSharing { self.dismissHomeMessage(message) } - pixelFiring.fire(.remoteMessageActionClicked, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + pixelFiring.fire(.remoteMessageActionClicked, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } case .primaryAction(let isSharing): if !isSharing { self.dismissHomeMessage(message) } - pixelFiring.fire(.remoteMessagePrimaryActionClicked, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + pixelFiring.fire(.remoteMessagePrimaryActionClicked, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } case .secondaryAction(let isSharing): if !isSharing { self.dismissHomeMessage(message) } - pixelFiring.fire(.remoteMessageSecondaryActionClicked, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + pixelFiring.fire(.remoteMessageSecondaryActionClicked, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } case .close: self.dismissHomeMessage(message) - pixelFiring.fire(.remoteMessageDismissed, - withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + if remoteMessage.isMetricsEnabled { + pixelFiring.fire(.remoteMessageDismissed, + withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) + } } } onDidAppear: { [weak self] in diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index 95298a90cd..6baf14af6a 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -111,7 +111,8 @@ private struct Constant { id: "0", content: .small(titleText: "Title", descriptionText: "Description"), matchingRules: [], - exclusionRules: [] + exclusionRules: [], + isMetricsEnabled: false ) ) ] diff --git a/DuckDuckGo/RemoteMessagingDebugViewController.swift b/DuckDuckGo/RemoteMessagingDebugViewController.swift new file mode 100644 index 0000000000..5527132af4 --- /dev/null +++ b/DuckDuckGo/RemoteMessagingDebugViewController.swift @@ -0,0 +1,113 @@ +// +// RemoteMessagingDebugViewController.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import SwiftUI +import RemoteMessaging +import Core +import Combine +import Persistence + +class RemoteMessagingDebugViewController: UIHostingController { + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: RemoteMessagingDebugRootView()) + } + +} + +struct RemoteMessagingDebugRootView: View { + + @ObservedObject var model = RemoteMessagingDebugViewModel() + + var body: some View { + List { + Section { + ForEach(model.messages, id: \.id) { entry in + VStack(alignment: .leading) { + Text(entry.id ?? "") + .font(.system(size: 14)) + Text(entry.message ?? "") + .font(.system(size: 12)) + Text("Shown: \(String(describing: entry.shown))") + .font(.system(size: 10)) + Text("Status: \(statusString(for: entry.status))") + .font(.system(size: 10)) + } + } + } footer: { + Text("This list contains messages that have been shown plus at most 1 message that is scheduled for showing. There may be more messages in the config that will be presented, but they haven't been processed yet.") + } + } + .navigationTitle("\(model.messages.count) Remote Messages") + .toolbar { + Button("Delete All", role: .destructive) { + model.deleteAll() + } + } + } + + /// This should be kept in sync with `RemoteMessageStatus` private enum from BSK + private func statusString(for status: NSNumber?) -> String { + switch status?.int16Value { + case 0: + return "scheduled" + case 1: + return "dismissed" + case 2: + return "done" + default: + return "unknown" + } + } +} + +class RemoteMessagingDebugViewModel: ObservableObject { + + @Published var messages: [RemoteMessageManagedObject] = [] + + let database: CoreDataDatabase + + init() { + database = Database.shared + fetchMessages() + } + + func deleteAll() { + let context = database.makeContext(concurrencyType: .mainQueueConcurrencyType) + context.deleteAll(entityDescriptions: [ + RemoteMessageManagedObject.entity(in: context), + RemoteMessagingConfigManagedObject.entity(in: context) + ]) + + do { + try context.save() + } catch { + assertionFailure("Failed to save after delete all") + } + fetchMessages() + } + + func fetchMessages() { + let context = database.makeContext(concurrencyType: .mainQueueConcurrencyType) + let fetchRequest = RemoteMessageManagedObject.fetchRequest() + fetchRequest.returnsObjectsAsFaults = false + messages = (try? context.fetch(fetchRequest)) ?? [] + } +} diff --git a/DuckDuckGoTests/NewTabPageMessagesModelTests.swift b/DuckDuckGoTests/NewTabPageMessagesModelTests.swift index 093ce16c68..f35d848f6a 100644 --- a/DuckDuckGoTests/NewTabPageMessagesModelTests.swift +++ b/DuckDuckGoTests/NewTabPageMessagesModelTests.swift @@ -186,6 +186,63 @@ final class NewTabPageMessagesModelTests: XCTestCase { XCTAssertEqual(PixelFiringMock.lastParams, [PixelParameters.message: "foo"]) } + func testDoesNotFirePixelOnCloseWhenMetricsAreDisabled() throws { + let sut = createSUT() + messagesConfiguration.homeMessages = [ + .mockRemote(withType: .small(titleText: "", descriptionText: ""), isMetricsEnabled: false), + ] + sut.load() + + let model = try XCTUnwrap(sut.homeMessageViewModels.first) + + model.onDidClose(.close) + + XCTAssertNil(PixelFiringMock.lastPixel) + XCTAssertNil(PixelFiringMock.lastParams) + } + + func testDoesNotFirePixelOnActionWhenMetricsAreDisabled() throws { + let sut = createSUT() + messagesConfiguration.homeMessages = [ + .mockRemote(withType: .small(titleText: "", descriptionText: ""), isMetricsEnabled: false), + ] + sut.load() + + let model = try XCTUnwrap(sut.homeMessageViewModels.first) + model.onDidClose(.action(isShare: false)) + + XCTAssertNil(PixelFiringMock.lastPixel) + XCTAssertNil(PixelFiringMock.lastParams) + } + + func testDoesNotFirePixelOnPrimaryActionWhenMetricsAreDisabled() throws { + let sut = createSUT() + messagesConfiguration.homeMessages = [ + .mockRemote(withType: .small(titleText: "", descriptionText: ""), isMetricsEnabled: false), + ] + sut.load() + + let model = try XCTUnwrap(sut.homeMessageViewModels.first) + model.onDidClose(.primaryAction(isShare: false)) + + XCTAssertNil(PixelFiringMock.lastPixel) + XCTAssertNil(PixelFiringMock.lastParams) + } + + func testDoesNotFirePixelOnSecondaryActionWhenMetricsAreDisabled() throws { + let sut = createSUT() + messagesConfiguration.homeMessages = [ + .mockRemote(withType: .small(titleText: "", descriptionText: ""), isMetricsEnabled: false), + ] + sut.load() + + let model = try XCTUnwrap(sut.homeMessageViewModels.first) + model.onDidClose(.secondaryAction(isShare: false)) + + XCTAssertNil(PixelFiringMock.lastPixel) + XCTAssertNil(PixelFiringMock.lastParams) + } + private func createSUT() -> NewTabPageMessagesModel { NewTabPageMessagesModel(homePageMessagesConfiguration: messagesConfiguration, notificationCenter: notificationCenter, @@ -217,7 +274,15 @@ private class HomePageMessagesConfigurationMock: HomePageMessagesConfiguration { } private extension HomeMessage { - static func mockRemote(withType type: RemoteMessageModelType) -> Self { - HomeMessage.remoteMessage(remoteMessage: .init(id: "foo", content: type, matchingRules: [], exclusionRules: [])) + static func mockRemote(withType type: RemoteMessageModelType, isMetricsEnabled: Bool = true) -> Self { + HomeMessage.remoteMessage( + remoteMessage: .init( + id: "foo", + content: type, + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: isMetricsEnabled + ) + ) } } From 56d955f189142637fd0aa8918b90ef4d93144d64 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 19 Jul 2024 11:58:39 +0100 Subject: [PATCH 42/48] avoid resizing webview when keyboard shows/hides (#3094) --- DuckDuckGo.xcodeproj/project.pbxproj | 4 - DuckDuckGo/AutocompleteSuggestionsModel.swift | 129 ------------------ DuckDuckGo/BrowserChromeManager.swift | 3 +- DuckDuckGo/MainView.swift | 24 ++-- DuckDuckGo/MainViewController.swift | 16 +-- DuckDuckGo/MainViewCoordinator.swift | 7 +- DuckDuckGo/TabViewController.swift | 36 ++++- 7 files changed, 52 insertions(+), 167 deletions(-) delete mode 100644 DuckDuckGo/AutocompleteSuggestionsModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a4dd4fccca..5205912fc8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -282,7 +282,6 @@ 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */; }; 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */; }; 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; - 6FDB3F192BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */; }; 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127372C20492500EB5724 /* NewTabPage.swift */; }; @@ -1429,7 +1428,6 @@ 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewModel.swift; sourceTree = ""; }; 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDelegate.swift; sourceTree = ""; }; 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; - 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteSuggestionsModel.swift; sourceTree = ""; }; 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FE127372C20492500EB5724 /* NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPage.swift; sourceTree = ""; }; @@ -5381,7 +5379,6 @@ isa = PBXGroup; children = ( F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */, - 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */, F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */, 8562CE142B9B645C00E1D399 /* CachedBookmarkSuggestions.swift */, 851672D02BED1FC900592F24 /* AutocompleteView.swift */, @@ -6748,7 +6745,6 @@ C1BF0BA529B63D7200482B73 /* AutofillLoginPromptHelper.swift in Sources */, D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */, F1F5337C1F26A9EF00D80D4F /* UserText.swift in Sources */, - 6FDB3F192BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift in Sources */, 1E8AD1C727BE9B2900ABA377 /* DownloadsListDataSource.swift in Sources */, 9FE08BDC2C2A88FA001D5EBC /* OnboardingIntroViewController.swift in Sources */, 3157B43527F497F50042D3D7 /* SaveLoginViewController.swift in Sources */, diff --git a/DuckDuckGo/AutocompleteSuggestionsModel.swift b/DuckDuckGo/AutocompleteSuggestionsModel.swift deleted file mode 100644 index cdbfe9d02f..0000000000 --- a/DuckDuckGo/AutocompleteSuggestionsModel.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// AutocompleteSuggestionsModel.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Suggestions - -struct AutocompleteSuggestionsModel { - - private let suggestions: [Suggestion] - private let sectionedSuggestions: [[IndexedSuggestion]] - - var isEmpty: Bool { suggestions.isEmpty } - var count: Int { suggestions.count } - - var numberOfSections: Int { sectionedSuggestions.count } - - init(suggestionsResult: SuggestionResult) { - self.suggestions = suggestionsResult.all - - let sectionsForDisplay = Self.makeSectionsForDisplay(using: suggestionsResult) - sectionedSuggestions = sectionsForDisplay - } - - func indexAfter(_ index: Int) -> Int { - (index + 1 >= count) ? 0 : index + 1 - } - - func indexBefore(_ index: Int) -> Int { - (index - 1 < 0) ? count - 1 : index - 1 - } - - func numberOfRows(in section: Int) -> Int { - guard sectionedSuggestions.indices.contains(section) else { return 0 } - - return sectionedSuggestions[section].count - } - - func suggestion(for index: Int) -> Suggestion? { - guard suggestions.indices.contains(index) else { return nil } - return suggestions[index] - } - - func index(for indexPath: IndexPath) -> Int? { - indexedSuggestion(for: indexPath)?.index - } - - func suggestion(for indexPath: IndexPath) -> Suggestion? { - indexedSuggestion(for: indexPath)?.suggestion - } - - func indexPath(for itemIndex: Int) -> IndexPath? { - guard suggestions.indices.contains(itemIndex) else { return nil } - - var section: Int = 0 - var row: Int = 0 - var currentIndex = itemIndex - - while true { - let currentSectionCount = sectionedSuggestions[section].count - if currentSectionCount > currentIndex { - row = currentIndex - break - } else { - currentIndex -= currentSectionCount - section += 1 - } - } - - return IndexPath(row: row, section: section) - } - - private func indexedSuggestion(for indexPath: IndexPath) -> IndexedSuggestion? { - guard sectionedSuggestions.indices.contains(indexPath.section), - sectionedSuggestions[indexPath.section].indices.contains(indexPath.row) else { - return nil - } - - return sectionedSuggestions[indexPath.section][indexPath.row] - } -} - -private extension AutocompleteSuggestionsModel { - static func makeSectionsForDisplay(using suggestionResult: SuggestionResult) -> [[IndexedSuggestion]] { - var index = -1 - var topResults = [IndexedSuggestion]() - var remoteSuggestions = [IndexedSuggestion]() - var auxResults = [IndexedSuggestion]() - - topResults = suggestionResult.topHits.map { - index += 1 - return IndexedSuggestion(index: index, suggestion: $0) - } - - remoteSuggestions = suggestionResult.duckduckgoSuggestions.map { - index += 1 - return IndexedSuggestion(index: index, suggestion: $0) - } - - auxResults = suggestionResult.localSuggestions.map { - index += 1 - return IndexedSuggestion(index: index, suggestion: $0) - } - - let results = [topResults, remoteSuggestions, auxResults] - - return results.filter { !$0.isEmpty } - } -} - -private struct IndexedSuggestion { - let index: Int - let suggestion: Suggestion -} diff --git a/DuckDuckGo/BrowserChromeManager.swift b/DuckDuckGo/BrowserChromeManager.swift index 9ec49d3d41..d7464d23fe 100644 --- a/DuckDuckGo/BrowserChromeManager.swift +++ b/DuckDuckGo/BrowserChromeManager.swift @@ -62,7 +62,8 @@ class BrowserChromeManager: NSObject, UIScrollViewDelegate { scrollView.delegate = self - observation = scrollView.observe(\.contentSize, options: .new) { [weak self] scrollView, _ in + observation = scrollView.observe(\.contentSize, options: .new) { [weak self] scrollView, observation in + guard observation.newValue != observation.oldValue else { return } self?.scrollViewDidResizeContent(scrollView) } } diff --git a/DuckDuckGo/MainView.swift b/DuckDuckGo/MainView.swift index 00839ce9ae..87b22aa6ad 100644 --- a/DuckDuckGo/MainView.swift +++ b/DuckDuckGo/MainView.swift @@ -214,25 +214,23 @@ extension MainViewFactory { } private func constrainNavigationBarContainer() { - let navigationBarContainer = coordinator.navigationBarContainer! + let container = coordinator.navigationBarContainer! let toolbar = coordinator.toolbar! let navigationBarCollectionView = coordinator.navigationBarCollectionView! - coordinator.constraints.navigationBarContainerTop = navigationBarContainer.constrainView(superview.safeAreaLayoutGuide, by: .top) - coordinator.constraints.navigationBarContainerBottom = navigationBarContainer.constrainView(toolbar, by: .bottom, to: .top) - coordinator.constraints.navigationBarCollectionViewBottom - = navigationBarCollectionView.constrainView(navigationBarContainer, by: .bottom, relatedBy: .greaterThanOrEqual) - + coordinator.constraints.navigationBarContainerTop = container.constrainView(superview.safeAreaLayoutGuide, by: .top) + coordinator.constraints.navigationBarContainerBottom = container.constrainView(toolbar, by: .bottom, to: .top) + coordinator.constraints.navigationBarContainerHeight = container.constrainAttribute(.height, to: 52, relatedBy: .equal) + NSLayoutConstraint.activate([ coordinator.constraints.navigationBarContainerTop, - navigationBarContainer.constrainView(superview, by: .leading), - navigationBarContainer.constrainView(superview, by: .trailing), - navigationBarContainer.constrainAttribute(.height, to: 52, relatedBy: .greaterThanOrEqual), + container.constrainView(superview, by: .leading), + container.constrainView(superview, by: .trailing), + coordinator.constraints.navigationBarContainerHeight, navigationBarCollectionView.constrainAttribute(.height, to: 52), - navigationBarCollectionView.constrainView(navigationBarContainer, by: .top), - navigationBarCollectionView.constrainView(navigationBarContainer, by: .leading), - navigationBarCollectionView.constrainView(navigationBarContainer, by: .trailing), - coordinator.constraints.navigationBarCollectionViewBottom + navigationBarCollectionView.constrainView(container, by: .top), + navigationBarCollectionView.constrainView(container, by: .leading), + navigationBarCollectionView.constrainView(container, by: .trailing), ]) } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 796a671420..57faf7eaa6 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -157,9 +157,7 @@ class MainViewController: UIViewController { // Skip SERP flow (focusing on autocomplete logic) and prepare for new navigation when selecting search bar private var skipSERPFlow = true - - private var keyboardHeight: CGFloat = 0.0 - + var postClear: (() -> Void)? var clearInProgress = false var dataStoreWarmup: DataStoreWarmup? = DataStoreWarmup() @@ -553,15 +551,14 @@ class MainViewController: UIViewController { let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue let animationCurve = UIView.AnimationOptions(rawValue: animationCurveRaw) - var height = keyboardFrame.size.height + var keyboardHeight = keyboardFrame.size.height let keyboardFrameInView = view.convert(keyboardFrame, from: nil) let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame.insetBy(dx: 0, dy: -additionalSafeAreaInsets.bottom) let intersection = safeAreaFrame.intersection(keyboardFrameInView) - height = intersection.height + keyboardHeight = intersection.height - findInPageBottomLayoutConstraint.constant = height - keyboardHeight = height + findInPageBottomLayoutConstraint.constant = keyboardHeight if let suggestionsTray = suggestionTrayController { let suggestionsFrameInView = suggestionsTray.view.convert(suggestionsTray.contentFrame, to: view) @@ -574,15 +571,14 @@ class MainViewController: UIViewController { } } - let y = self.view.frame.height - height + let y = self.view.frame.height - keyboardHeight let frame = self.findInPageView.frame UIView.animate(withDuration: duration, delay: 0, options: animationCurve, animations: { self.findInPageView.frame = CGRect(x: 0, y: y - frame.height, width: frame.width, height: frame.height) }, completion: nil) if self.appSettings.currentAddressBarPosition.isBottom { - let navBarOffset = min(0, self.toolbarHeight - intersection.height) - self.viewCoordinator.constraints.navigationBarCollectionViewBottom.constant = navBarOffset + self.viewCoordinator.constraints.navigationBarContainerHeight.constant = max(52, keyboardHeight) UIView.animate(withDuration: duration, delay: 0, options: animationCurve) { self.viewCoordinator.navigationBarContainer.superview?.layoutIfNeeded() } diff --git a/DuckDuckGo/MainViewCoordinator.swift b/DuckDuckGo/MainViewCoordinator.swift index ccd08da1f7..3004991b75 100644 --- a/DuckDuckGo/MainViewCoordinator.swift +++ b/DuckDuckGo/MainViewCoordinator.swift @@ -65,7 +65,7 @@ class MainViewCoordinator { var navigationBarContainerTop: NSLayoutConstraint! var navigationBarContainerBottom: NSLayoutConstraint! - var navigationBarCollectionViewBottom: NSLayoutConstraint! + var navigationBarContainerHeight: NSLayoutConstraint! var toolbarBottom: NSLayoutConstraint! var contentContainerTop: NSLayoutConstraint! var tabBarContainerTop: NSLayoutConstraint! @@ -137,14 +137,10 @@ class MainViewCoordinator { return } - constraints.contentContainerBottomToToolbarTop.isActive = false - constraints.contentContainerBottomToNavigationBarContainerTop.isActive = true - navigationBarContainer.isHidden = false } func setAddressBarTopActive(_ active: Bool) { - constraints.contentContainerBottomToToolbarTop.isActive = active constraints.navigationBarContainerTop.isActive = active constraints.progressBarTop.isActive = active constraints.topSlideContainerBottomToNavigationBarBottom.isActive = active @@ -152,7 +148,6 @@ class MainViewCoordinator { } func setAddressBarBottomActive(_ active: Bool) { - constraints.contentContainerBottomToNavigationBarContainerTop.isActive = active constraints.progressBarBottom.isActive = active constraints.navigationBarContainerBottom.isActive = active constraints.topSlideContainerBottomToStatusBackgroundBottom.isActive = active diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 5e86fcf988..43d494d4e4 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -55,7 +55,8 @@ class TabViewController: UIViewController { @IBOutlet private(set) weak var errorHeader: UILabel! @IBOutlet private(set) weak var errorMessage: UILabel! @IBOutlet weak var webViewContainer: UIView! - + var webViewBottomAnchorConstraint: NSLayoutConstraint? + @IBOutlet var showBarsTapGestureRecogniser: UITapGestureRecognizer! private let instrumentation = TabInstrumentation() @@ -344,7 +345,8 @@ class TabViewController: UIViewController { addTextSizeObserver() subscribeToEmailProtectionSignOutNotification() registerForDownloadsNotifications() - + registerForAddressBarLocationNotifications() + // Setup DuckPlayer navigation handler self.youtubeNavigationHandler = YoutubePlayerNavigationHandler(duckPlayer: duckPlayer) @@ -357,7 +359,13 @@ class TabViewController: UIViewController { #endif } - + private func registerForAddressBarLocationNotifications() { + NotificationCenter.default.addObserver(self, selector: + #selector(onAddressBarPositionChanged), + name: AppUserDefaults.Notifications.addressBarPositionChanged, + object: nil) + } + @available(iOS 16.4, *) private func registerForInspectableWebViewNotifications() { NotificationCenter.default.addObserver(self, @@ -375,6 +383,16 @@ class TabViewController: UIViewController { #endif } + @objc + private func onAddressBarPositionChanged() { + updateWebViewBottomAnchor() + } + + private func updateWebViewBottomAnchor() { + let targetHeight = chromeDelegate?.barsMaxHeight ?? 0.0 + webViewBottomAnchorConstraint?.constant = appSettings.currentAddressBarPosition == .bottom ? -targetHeight : 0 + } + private func observeNetPConnectionStatusChanges() { netPConnectionObserverCancellable = netPConnectionObserver.publisher .receive(on: DispatchQueue.main) @@ -387,6 +405,7 @@ class TabViewController: UIViewController { userScripts?.autofillUserScript.emailDelegate = emailManager woShownRecently = false // don't fire if the user goes somewhere else first + updateWebViewBottomAnchor() resetNavigationBar() delegate?.tabDidRequestShowingMenuHighlighter(tab: self) tabModel.viewed = true @@ -442,7 +461,6 @@ class TabViewController: UIViewController { userContentController.delegate = self webView = WKWebView(frame: view.bounds, configuration: configuration) - webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] webView.allowsLinkPreview = true webView.allowsBackForwardNavigationGestures = true @@ -451,7 +469,17 @@ class TabViewController: UIViewController { webView.navigationDelegate = self webView.uiDelegate = self + webViewContainer.addSubview(webView) + webView.translatesAutoresizingMaskIntoConstraints = false + webViewBottomAnchorConstraint = webView.bottomAnchor.constraint(equalTo: webViewContainer.bottomAnchor) + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: webViewContainer.topAnchor), + webView.leadingAnchor.constraint(equalTo: webViewContainer.leadingAnchor), + webViewBottomAnchorConstraint!, + webView.trailingAnchor.constraint(equalTo: webViewContainer.trailingAnchor) + ]) + webView.scrollView.refreshControl = refreshControl // Be sure to set `tintColor` after the control is attached to ScrollView otherwise haptics are gone. // We don't have to care about it for this control instance the next time `setRefreshControlEnabled` From 9eb684bb2a7854752b1cda2bccb367c995558e43 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 19 Jul 2024 13:18:34 +0200 Subject: [PATCH 43/48] Fix VPN configuration removal to stop the tunnel (#3099) Task/Issue URL: https://app.asana.com/0/1207603085593419/1207832283330964/f macOS PR: https://github.com/duckduckgo/macos-browser/pull/2991 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/900 ## Description: Fix the VPN configuration removal handling in the extension to stop the tunnel. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../xcshareddata/xcschemes/DuckDuckGo.xcscheme | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5205912fc8..1777075ca0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10166,7 +10166,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 172.0.0; + version = 172.0.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index aea2579038..518224f0cc 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "f8e381771a33287e317da84d5676a5e2a271bca3", - "version" : "172.0.0" + "revision" : "3274feb8d84fda5f27541c13f2ab428b4e77a5e2", + "version" : "172.0.1" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index 093c088c63..821a6a6a5e 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -64,6 +64,9 @@ + + Date: Fri, 19 Jul 2024 15:18:36 +0200 Subject: [PATCH 44/48] New Tab Page Shortcuts section (#3104) --- DuckDuckGo.xcodeproj/project.pbxproj | 16 ++++ ...ofillLoginSettingsListViewController.swift | 1 + DuckDuckGo/MainViewController.swift | 27 +++++++ DuckDuckGo/NewTabPageControllerDelegate.swift | 8 ++ DuckDuckGo/NewTabPageShortcut.swift | 43 ++++++++++ DuckDuckGo/NewTabPageView.swift | 26 ++++-- DuckDuckGo/NewTabPageViewController.swift | 26 +++++- DuckDuckGo/ShortcutAccessoryView.swift | 79 +++++++++++++++++++ DuckDuckGo/ShortcutItemView.swift | 74 +++++++++++++++-- .../AI-Chat-Color-32.svg | 12 +++ .../AI-Chat-Color-32.imageset/Contents.json | 15 ++++ .../Bookmarks-Color-32.svg | 14 ++++ .../Bookmarks-Color-32.imageset/Contents.json | 15 ++++ DuckDuckGo/Shortcuts.xcassets/Contents.json | 6 ++ .../Downloads-Color-32.imageset/Contents.json | 15 ++++ .../Downloads-Color-32.svg | 5 ++ .../Contents.json | 15 ++++ .../Passwords-Autofill-Color-32.svg | 5 ++ .../Settings-Color-32.imageset/Contents.json | 15 ++++ .../Settings-Color-32.svg | 11 +++ DuckDuckGo/ShortcutsModel.swift | 46 +++++++++++ DuckDuckGo/ShortcutsView.swift | 35 +++----- DuckDuckGo/UserText.swift | 10 ++- DuckDuckGo/en.lproj/Localizable.strings | 27 ++++--- 24 files changed, 492 insertions(+), 54 deletions(-) create mode 100644 DuckDuckGo/NewTabPageShortcut.swift create mode 100644 DuckDuckGo/ShortcutAccessoryView.swift create mode 100644 DuckDuckGo/Shortcuts.xcassets/AI-Chat-Color-32.imageset/AI-Chat-Color-32.svg create mode 100644 DuckDuckGo/Shortcuts.xcassets/AI-Chat-Color-32.imageset/Contents.json create mode 100644 DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Bookmarks-Color-32.svg create mode 100644 DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Contents.json create mode 100644 DuckDuckGo/Shortcuts.xcassets/Contents.json create mode 100644 DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Contents.json create mode 100644 DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Downloads-Color-32.svg create mode 100644 DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Contents.json create mode 100644 DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Passwords-Autofill-Color-32.svg create mode 100644 DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Contents.json create mode 100644 DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Settings-Color-32.svg create mode 100644 DuckDuckGo/ShortcutsModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1777075ca0..f4a7b3516b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -261,6 +261,10 @@ 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */; }; 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; }; + 6F64AA592C4818D700CF4489 /* NewTabPageShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */; }; + 6F64AA5B2C481AAA00CF4489 /* Shortcuts.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */; }; + 6F64AA5D2C4920D200CF4489 /* ShortcutAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA5C2C4920D200CF4489 /* ShortcutAccessoryView.swift */; }; + 6F64AA5F2C49463C00CF4489 /* ShortcutsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA5E2C49463C00CF4489 /* ShortcutsModel.swift */; }; 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; }; 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; }; 6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; }; @@ -1406,6 +1410,10 @@ 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonView.swift; sourceTree = ""; }; 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = ""; }; + 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcut.swift; sourceTree = ""; }; + 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Shortcuts.xcassets; sourceTree = ""; }; + 6F64AA5C2C4920D200CF4489 /* ShortcutAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutAccessoryView.swift; sourceTree = ""; }; + 6F64AA5E2C49463C00CF4489 /* ShortcutsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsModel.swift; sourceTree = ""; }; 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = ""; }; 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = ""; }; 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = ""; }; @@ -3569,6 +3577,10 @@ children = ( 6FE1273F2C204D9B00EB5724 /* ShortcutsView.swift */, 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */, + 6F64AA5C2C4920D200CF4489 /* ShortcutAccessoryView.swift */, + 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */, + 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */, + 6F64AA5E2C49463C00CF4489 /* ShortcutsModel.swift */, ); name = Shortcuts; sourceTree = ""; @@ -6328,6 +6340,7 @@ F4F7F10C25813FE200045D62 /* 03_Airstream_divided_by_four.json in Resources */, AAF2E28723E0498200962AF8 /* AppIconPurple83.5x83.5@2x.png in Resources */, AA4D6AB923DE4D15007E8790 /* AppIconYellow29x29@3x.png in Resources */, + 6F64AA5B2C481AAA00CF4489 /* Shortcuts.xcassets in Resources */, 984147B424F0264B00362052 /* Authentication.storyboard in Resources */, 1EE411FD2858B9300003FE64 /* dark-trackers-2.json in Resources */, AA4D6ABC23DE4D15007E8790 /* AppIconYellow60x60@3x.png in Resources */, @@ -6673,6 +6686,7 @@ 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */, 1DEAADE82BA38AA500E25A97 /* SettingsGeneralView.swift in Sources */, 853C5F5B21BFF0AE001F7A05 /* HomeCollectionView.swift in Sources */, + 6F64AA592C4818D700CF4489 /* NewTabPageShortcut.swift in Sources */, 3132FA2627A0784600DD7A12 /* FilePreviewHelper.swift in Sources */, 9820FF502244FECC008D4782 /* UIScrollViewExtension.swift in Sources */, 8540BD5423D8D5080057FDD2 /* PreserveLoginsAlert.swift in Sources */, @@ -6897,6 +6911,7 @@ 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */, 3151F0EE2735800800226F58 /* VoiceSearchFeedbackView.swift in Sources */, 37CF91642BB4A82A00BADCAE /* CrashCollectionOnboardingViewModel.swift in Sources */, + 6F64AA5D2C4920D200CF4489 /* ShortcutAccessoryView.swift in Sources */, 857EEB752095FFAC008A005C /* HomeRowInstructionsViewController.swift in Sources */, D63FF8952C1B67E9006DE24D /* YoutubePlayerUserScript.swift in Sources */, 4BF3E4AF2C06A85200ED5D57 /* VPNRedditSessionWorkaround.swift in Sources */, @@ -6990,6 +7005,7 @@ 85C861E628FF1B5F00189466 /* HomeViewSectionRenderersExtension.swift in Sources */, CB825C922C071B1400BCC586 /* AlertView.swift in Sources */, 1DDF40292BA04FCD006850D9 /* SettingsPrivacyProtectionsView.swift in Sources */, + 6F64AA5F2C49463C00CF4489 /* ShortcutsModel.swift in Sources */, F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */, 85F2FFCD2211F615006BB258 /* MainViewController+KeyCommands.swift in Sources */, 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */, diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index 500f9f3216..fba8246fca 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -33,6 +33,7 @@ enum AutofillSettingsSource: String { case appIconShortcut = "app_icon_shortcut" case homeScreenWidget = "home_screen_widget" case lockScreenWidget = "lock_screen_widget" + case newTabPageShortcut = "new_tab_page_shortcut" } protocol AutofillLoginSettingsListViewControllerDelegate: AnyObject { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 57faf7eaa6..4399ff1653 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -738,6 +738,7 @@ class MainViewController: UIViewController { homePageMessagesConfiguration: homePageConfiguration) controller.delegate = self + controller.shortcutsDelegate = self newTabPageViewController = controller addToContentContainer(controller: controller) @@ -2026,6 +2027,32 @@ extension MainViewController: NewTabPageControllerDelegate { } } +extension MainViewController: NewTabPageControllerShortcutsDelegate { + func newTabPageDidRequestDownloads(_ controller: NewTabPageViewController) { + segueToDownloads() + } + + func newTabPageDidRequestBookmarks(_ controller: NewTabPageViewController) { + segueToBookmarks() + } + + func newTabPageDidRequestPasswords(_ controller: NewTabPageViewController) { + launchAutofillLogins(source: .newTabPageShortcut) + } + + func newTabPageDidRequestAIChat(_ controller: NewTabPageViewController) { + loadUrl(Constant.duckAIURL) + } + + func newTabPageDidRequestSettings(_ controller: NewTabPageViewController) { + segueToSettings() + } + + private enum Constant { + static let duckAIURL = URL(string: "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1")! + } +} + extension MainViewController: TabDelegate { func tab(_ tab: TabViewController, diff --git a/DuckDuckGo/NewTabPageControllerDelegate.swift b/DuckDuckGo/NewTabPageControllerDelegate.swift index 15981c4b5d..08ecc46c65 100644 --- a/DuckDuckGo/NewTabPageControllerDelegate.swift +++ b/DuckDuckGo/NewTabPageControllerDelegate.swift @@ -25,3 +25,11 @@ protocol NewTabPageControllerDelegate: AnyObject { func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) func newTabPageDidEditFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) } + +protocol NewTabPageControllerShortcutsDelegate: AnyObject { + func newTabPageDidRequestDownloads(_ controller: NewTabPageViewController) + func newTabPageDidRequestBookmarks(_ controller: NewTabPageViewController) + func newTabPageDidRequestPasswords(_ controller: NewTabPageViewController) + func newTabPageDidRequestAIChat(_ controller: NewTabPageViewController) + func newTabPageDidRequestSettings(_ controller: NewTabPageViewController) +} diff --git a/DuckDuckGo/NewTabPageShortcut.swift b/DuckDuckGo/NewTabPageShortcut.swift new file mode 100644 index 0000000000..26674e63c9 --- /dev/null +++ b/DuckDuckGo/NewTabPageShortcut.swift @@ -0,0 +1,43 @@ +// +// NewTabPageShortcut.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +enum NewTabPageShortcut: CaseIterable, Equatable, Identifiable, Codable { + var id: String { storageIdentifier } + + case bookmarks, aiChat, passwords, downloads, settings +} + +extension NewTabPageShortcut { + var storageIdentifier: String { + switch self { + case .bookmarks: + "shortcut.storage.identifier.bookmarks" + case .aiChat: + "shortcut.storage.identifier.aichat" + case .passwords: + "shortcut.storage.identifier.passwords" + case .downloads: + "shortcut.storage.identifier.downloads" + case .settings: + "shortcut.storage.identifier.settings" + } + } +} diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index 6baf14af6a..0c3af6f7fe 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -24,12 +24,14 @@ import RemoteMessaging struct NewTabPageView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject var messagesModel: NewTabPageMessagesModel - @ObservedObject var favoritesModel: FavoritesModelType + @ObservedObject private var messagesModel: NewTabPageMessagesModel + @ObservedObject private var favoritesModel: FavoritesModelType + @ObservedObject private var shortcutsModel: ShortcutsModel - init(messagesModel: NewTabPageMessagesModel, favoritesModel: FavoritesModelType) { + init(messagesModel: NewTabPageMessagesModel, favoritesModel: FavoritesModelType, shortcutsModel: ShortcutsModel) { self.messagesModel = messagesModel self.favoritesModel = favoritesModel + self.shortcutsModel = shortcutsModel self.messagesModel.load() } @@ -55,8 +57,10 @@ struct NewTabPageView: View { } // MARK: Shortcuts - ShortcutsView() - .padding(Constant.sectionPadding) + if !shortcutsModel.enabledShortcuts.isEmpty { + ShortcutsView(model: shortcutsModel) + .padding(Constant.sectionPadding) + } Spacer() @@ -97,7 +101,8 @@ private struct Constant { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel() + favoritesModel: FavoritesPreviewModel(), + shortcutsModel: ShortcutsModel() ) } @@ -118,7 +123,8 @@ private struct Constant { ] ) ), - favoritesModel: FavoritesPreviewModel() + favoritesModel: FavoritesPreviewModel(), + shortcutsModel: ShortcutsModel() ) } @@ -141,3 +147,9 @@ private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration homeMessages = homeMessages.dropLast() } } + +private extension ShortcutsModel { + convenience init() { + self.init(shortcutsPreferencesStorage: InMemoryShortcutsPreferencesStorage()) + } +} diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index 6b187d8660..2d5c92fc57 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -30,6 +30,7 @@ final class NewTabPageViewController: UIHostingController + + + + + + + + + + + diff --git a/DuckDuckGo/Shortcuts.xcassets/AI-Chat-Color-32.imageset/Contents.json b/DuckDuckGo/Shortcuts.xcassets/AI-Chat-Color-32.imageset/Contents.json new file mode 100644 index 0000000000..7ccc364224 --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/AI-Chat-Color-32.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "AI-Chat-Color-32.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Bookmarks-Color-32.svg b/DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Bookmarks-Color-32.svg new file mode 100644 index 0000000000..1f3b554aae --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Bookmarks-Color-32.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Contents.json b/DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Contents.json new file mode 100644 index 0000000000..c2a391a6cd --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Bookmarks-Color-32.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Bookmarks-Color-32.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Shortcuts.xcassets/Contents.json b/DuckDuckGo/Shortcuts.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Contents.json b/DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Contents.json new file mode 100644 index 0000000000..9b57292103 --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Downloads-Color-32.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Downloads-Color-32.svg b/DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Downloads-Color-32.svg new file mode 100644 index 0000000000..950c33172e --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Downloads-Color-32.imageset/Downloads-Color-32.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Contents.json b/DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Contents.json new file mode 100644 index 0000000000..211f5e0af8 --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Passwords-Autofill-Color-32.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Passwords-Autofill-Color-32.svg b/DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Passwords-Autofill-Color-32.svg new file mode 100644 index 0000000000..371a50c006 --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Passwords-Autofill-Color-32.imageset/Passwords-Autofill-Color-32.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Contents.json b/DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Contents.json new file mode 100644 index 0000000000..9171449b3d --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Settings-Color-32.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Settings-Color-32.svg b/DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Settings-Color-32.svg new file mode 100644 index 0000000000..07bca140f7 --- /dev/null +++ b/DuckDuckGo/Shortcuts.xcassets/Settings-Color-32.imageset/Settings-Color-32.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/DuckDuckGo/ShortcutsModel.swift b/DuckDuckGo/ShortcutsModel.swift new file mode 100644 index 0000000000..21ef76d50a --- /dev/null +++ b/DuckDuckGo/ShortcutsModel.swift @@ -0,0 +1,46 @@ +// +// ShortcutsModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol ShortcutsPreferencesStorage { + var enabledShortcuts: [NewTabPageShortcut] { get } +} + +final class ShortcutsModel: ObservableObject { + @Published private(set) var enabledShortcuts: [NewTabPageShortcut] = [] + + private let shortcutsPreferencesStorage: ShortcutsPreferencesStorage + + var onShortcutOpened: ((NewTabPageShortcut) -> Void)? + + init(shortcutsPreferencesStorage: ShortcutsPreferencesStorage) { + self.shortcutsPreferencesStorage = shortcutsPreferencesStorage + + enabledShortcuts = shortcutsPreferencesStorage.enabledShortcuts + } + + func openShortcut(_ shortcut: NewTabPageShortcut) { + onShortcutOpened?(shortcut) + } +} + +final class InMemoryShortcutsPreferencesStorage: ShortcutsPreferencesStorage { + private(set) var enabledShortcuts: [NewTabPageShortcut] = NewTabPageShortcut.allCases +} diff --git a/DuckDuckGo/ShortcutsView.swift b/DuckDuckGo/ShortcutsView.swift index cb3bb026d0..3ee93cf3c2 100644 --- a/DuckDuckGo/ShortcutsView.swift +++ b/DuckDuckGo/ShortcutsView.swift @@ -19,38 +19,25 @@ import SwiftUI -enum Shortcut: Int, CaseIterable, Equatable, Identifiable { - var id: Int { rawValue } - - case bookmarks, aiChat, vpn, passwords - - var name: String { - switch self { - case .bookmarks: - UserText.homeTabShortcutBookmarks - case .aiChat: - UserText.homeTabShortcutAIChat - case .vpn: - UserText.homeTabShortcutVPN - case .passwords: - UserText.homeTabShortcutPasswords - } - } -} - struct ShortcutsView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @State var enabledShortcuts: [Shortcut] = Array(Shortcut.allCases.prefix(upTo: 3)) + @ObservedObject private(set) var model: ShortcutsModel var body: some View { NewTabPageGridView { _ in - ForEach(enabledShortcuts) { shortcut in - ShortcutItemView(name: shortcut.name) + ForEach(model.enabledShortcuts) { shortcut in + Button { + model.openShortcut(shortcut) + } label: { + ShortcutItemView(shortcut: shortcut, accessoryType: nil) + } } } } } #Preview { - ShortcutsView() + ScrollView { + ShortcutsView(model: ShortcutsModel(shortcutsPreferencesStorage: InMemoryShortcutsPreferencesStorage())) + } + .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 0464d99144..13bd95fe9d 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1176,10 +1176,12 @@ But if you *do* want a peek under the hood, you can find more information about public static let duckPlayerPresentationModalDismissButton = NSLocalizedString("duckplayer.presentation.modal.dismiss-button", value: "Got it!", comment: "Button that will dismiss the modal") // Home Tab Shortcuts - public static let homeTabShortcutBookmarks = NSLocalizedString("home.tab.shortcut.bookmarks", value: "Bookmarks", comment: "Shortcut title leading to Bookmarks") - public static let homeTabShortcutAIChat = NSLocalizedString("home.tab.shortcut.ai.chat", value: "AI Chat", comment: "Shortcut title leading to AI Chat") - public static let homeTabShortcutVPN = NSLocalizedString("home.tab.shortcut.vpn", value: "VPN", comment: "Shortcut title leading to VPN") - public static let homeTabShortcutPasswords = NSLocalizedString("home.tab.shortcut.passwords", value: "Passwords", comment: "Shortcut title leading to Passwords") + public static let newTabPageShortcutBookmarks = NSLocalizedString("new.tab.page.shortcut.bookmarks", value: "Bookmarks", comment: "Shortcut title leading to Bookmarks") + public static let newTabPageShortcutAIChat = NSLocalizedString("new.tab.page.shortcut.ai.chat", value: "AI Chat", comment: "Shortcut title leading to AI Chat") + public static let newTabPageShortcutPasswords = NSLocalizedString("new.tab.page.shortcut.passwords", value: "Passwords", comment: "Shortcut title leading to Passwords") + + public static let newTabPageShortcutDownloads = NSLocalizedString("new.tab.page.shortcut.downloads", value: "Downloads", comment: "Shortcut title leading to Downloads") + public static let newTabPageShortcutSettings = NSLocalizedString("new.tab.page.shortcut.settings", value: "Settings", comment: "Shortcut title leading to app settings") // MARK: - Dax Onboarding Experiment public enum DaxOnboardingExperiment { diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index e28055ff28..9e71046420 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1249,18 +1249,6 @@ /* Home is this context is the bottom home row (dock) */ "home.row.reminder.title" = "Take DuckDuckGo home"; -/* Shortcut title leading to AI Chat */ -"home.tab.shortcut.ai.chat" = "AI Chat"; - -/* Shortcut title leading to Bookmarks */ -"home.tab.shortcut.bookmarks" = "Bookmarks"; - -/* Shortcut title leading to Passwords */ -"home.tab.shortcut.passwords" = "Passwords"; - -/* Shortcut title leading to VPN */ -"home.tab.shortcut.vpn" = "VPN"; - /* This describes empty tab */ "homeTab.searchAndFavorites" = "Search or enter address"; @@ -1576,6 +1564,21 @@ https://duckduckgo.com/mac"; /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Settings"; +/* Shortcut title leading to AI Chat */ +"new.tab.page.shortcut.ai.chat" = "AI Chat"; + +/* Shortcut title leading to Bookmarks */ +"new.tab.page.shortcut.bookmarks" = "Bookmarks"; + +/* Shortcut title leading to Downloads */ +"new.tab.page.shortcut.downloads" = "Downloads"; + +/* Shortcut title leading to Passwords */ +"new.tab.page.shortcut.passwords" = "Passwords"; + +/* Shortcut title leading to app settings */ +"new.tab.page.shortcut.settings" = "Settings"; + /* Do not translate - stringsdict entry */ "number.of.tabs" = "number.of.tabs"; From 01a1c386db1b789f98143902cfc88f8168ba0d1e Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 19 Jul 2024 18:02:02 +0200 Subject: [PATCH 45/48] [DuckPlayer] 6 - Init updates and Watch on YouTube (#3066) Task/Issue URL: https://app.asana.com/0/1201141132935289/1207777888338886/f Description: Updates DuckPlayer initialization requirements Renames Youtubplayernavhandler to DuckPlayernavhandler Adds DuckPlayer logging Opens video on Youtube when you tap "Watch in Youtube" button Opens video on Youtube player when you tap the "Youtube" logo in the player itself --- Core/Logging.swift | 5 +- Core/UserDefaultsPropertyWrapper.swift | 3 +- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../DuckPlayer/DuckNavigationHandling.swift | 9 +- DuckDuckGo/DuckPlayer/DuckPlayer.swift | 55 ++++- ...wift => DuckPlayerNavigationHandler.swift} | 118 +++++++--- .../DuckPlayer/DuckPlayerURLExtension.swift | 10 +- .../DuckPlayer/YoutubeOverlayUserScript.swift | 2 +- .../DuckPlayer/YoutubePlayerUserScript.swift | 2 +- DuckDuckGo/TabManager.swift | 10 +- DuckDuckGo/TabViewController.swift | 52 ++--- ...ViewControllerLongPressMenuExtension.swift | 3 +- DuckDuckGoTests/DuckPlayerMocks.swift | 8 + .../DuckPlayerURLExtensionTests.swift | 50 +++-- ...YoutublePlayerNavigationHandlerTests.swift | 206 +++++++++--------- submodules/privacy-reference-tests | 2 +- 17 files changed, 324 insertions(+), 221 deletions(-) rename DuckDuckGo/DuckPlayer/{YouTubePlayerNavigationHandler.swift => DuckPlayerNavigationHandler.swift} (64%) diff --git a/Core/Logging.swift b/Core/Logging.swift index fd6bdac037..9be20c9153 100644 --- a/Core/Logging.swift +++ b/Core/Logging.swift @@ -31,6 +31,7 @@ public extension OSLog { case autoconsentLog = "DDG Autoconsent" case configurationLog = "DDG Configuration" case syncLog = "DDG Sync" + case duckPlayerLog = "Duck Player" } @OSLogWrapper(.generalLog) static var generalLog @@ -40,6 +41,7 @@ public extension OSLog { @OSLogWrapper(.autoconsentLog) static var autoconsentLog @OSLogWrapper(.configurationLog) static var configurationLog @OSLogWrapper(.syncLog) static var syncLog + @OSLogWrapper(.duckPlayerLog) static var duckPlayerLog // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // To activate Logging Categories add categories here: @@ -50,7 +52,8 @@ public extension OSLog { .adAttributionLog, .lifecycleLog, .configurationLog, - .syncLog + .syncLog, + .duckPlayerLog ] #endif diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index dd3d93e456..94c8cdc6f6 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -150,9 +150,10 @@ public struct UserDefaultsWrapper { case duckPlayerMode = "com.duckduckgo.ios.duckPlayerMode" case duckPlayerAskModeOverlayHidden = "com.duckduckgo.ios.duckPlayerAskModeOverlayHidden" - + case vpnRedditWorkaroundInstalled = "com.duckduckgo.ios.vpn.workaroundInstalled" + // Debug keys case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4a7b3516b..cc96638f15 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -830,7 +830,7 @@ D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; D65625902C22D307006EF297 /* DuckPlayerURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63FF88B2C1B21ED006DE24D /* DuckPlayerURLExtension.swift */; }; - D65625922C22D340006EF297 /* YouTubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63FF8892C1B21C2006DE24D /* YouTubePlayerNavigationHandler.swift */; }; + D65625922C22D340006EF297 /* DuckPlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */; }; D65625952C22D382006EF297 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */; }; D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65625A02C232F5E006EF297 /* SettingsDuckPlayerView.swift */; }; D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; @@ -2512,7 +2512,7 @@ D62EC3C12C248AF800FC9D04 /* DuckNavigationHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckNavigationHandling.swift; sourceTree = ""; }; D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailManagerRequestDelegate.swift; sourceTree = ""; }; D63677F42BBDB1C300605BA5 /* DaxLogoNavbarTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxLogoNavbarTitle.swift; sourceTree = ""; }; - D63FF8892C1B21C2006DE24D /* YouTubePlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubePlayerNavigationHandler.swift; sourceTree = ""; }; + D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerNavigationHandler.swift; sourceTree = ""; }; D63FF88B2C1B21ED006DE24D /* DuckPlayerURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerURLExtension.swift; sourceTree = ""; }; D63FF8932C1B67E8006DE24D /* YoutubePlayerUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerUserScript.swift; sourceTree = ""; }; D63FF8942C1B67E8006DE24D /* YoutubeOverlayUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScript.swift; sourceTree = ""; }; @@ -4730,7 +4730,7 @@ D63FF8972C1B6A45006DE24D /* DuckPlayer.swift */, D6037E682C32F2E7009AAEC0 /* DuckPlayerSettings.swift */, D62EC3C12C248AF800FC9D04 /* DuckNavigationHandling.swift */, - D63FF8892C1B21C2006DE24D /* YouTubePlayerNavigationHandler.swift */, + D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */, D63FF88B2C1B21ED006DE24D /* DuckPlayerURLExtension.swift */, D63FF8942C1B67E8006DE24D /* YoutubeOverlayUserScript.swift */, D63FF8932C1B67E8006DE24D /* YoutubePlayerUserScript.swift */, @@ -6972,7 +6972,7 @@ 1EA51376286596A000493C6A /* PrivacyIconLogic.swift in Sources */, 980891A92238504B00313A70 /* UILabelExtension.swift in Sources */, 984D035A24ACCC7D0066CFB8 /* TabViewCell.swift in Sources */, - D65625922C22D340006EF297 /* YouTubePlayerNavigationHandler.swift in Sources */, + D65625922C22D340006EF297 /* DuckPlayerNavigationHandler.swift in Sources */, 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */, F194FAED1F14E2B3009B4DF8 /* UIFontExtension.swift in Sources */, 98F0FC2021FF18E700CE77AB /* AutoClearSettingsViewController.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 518224f0cc..a8a4f9f886 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift b/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift index c2e2178472..33ad44fb25 100644 --- a/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift +++ b/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift @@ -21,13 +21,10 @@ import WebKit protocol DuckNavigationHandling { var referrer: DuckPlayerReferrer { get set } - func handleNavigation(_ navigationAction: WKNavigationAction, - webView: WKWebView, - completion: @escaping (WKNavigationActionPolicy) -> Void) + var duckPlayer: DuckPlayerProtocol { get } + func handleNavigation(_ navigationAction: WKNavigationAction, webView: WKWebView) func handleURLChange(url: URL?, webView: WKWebView) - func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, - completion: @escaping (WKNavigationActionPolicy) -> Void, - webView: WKWebView) + func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, webView: WKWebView) func handleGoBack(webView: WKWebView) func handleReload(webView: WKWebView) } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayer.swift b/DuckDuckGo/DuckPlayer/DuckPlayer.swift index fb54fd00b9..df7e5ecf29 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayer.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayer.swift @@ -26,7 +26,7 @@ import UserScript import Core /// Values that the Frontend can use to determine the current state. -struct InitialSetupSettings: Codable { +struct InitialPlayerSettings: Codable { struct PlayerSettings: Codable { let pip: PIP } @@ -34,16 +34,36 @@ struct InitialSetupSettings: Codable { struct PIP: Codable { let status: Status } + + struct Platform: Codable { + let name: String + } enum Status: String, Codable { case enabled case disabled } + + enum Environment: String, Codable { + case development + case production + } + + enum Locale: String, Codable { + case en + } let userValues: UserValues let settings: PlayerSettings + let platform: Platform + let locale: Locale } +struct InitialOverlaySettings: Codable { + let userValues: UserValues +} + + /// Values that the Frontend can use to determine user settings public struct UserValues: Codable { enum CodingKeys: String, CodingKey { @@ -67,7 +87,9 @@ protocol DuckPlayerProtocol { func setUserValues(params: Any, message: WKScriptMessage) -> Encodable? func getUserValues(params: Any, message: WKScriptMessage) -> Encodable? func openVideoInDuckPlayer(url: URL, webView: WKWebView) - func initialSetup(params: Any, message: WKScriptMessage) async -> Encodable? + + func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> Encodable? + func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> Encodable? } final class DuckPlayer: DuckPlayerProtocol { @@ -103,9 +125,15 @@ final class DuckPlayer: DuckPlayerProtocol { } @MainActor - public func initialSetup(params: Any, message: WKScriptMessage) async -> Encodable? { + public func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> Encodable? { let webView = message.webView - return await self.encodedSettings(with: webView) + return await self.encodedPlayerSettings(with: webView) + } + + @MainActor + public func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> Encodable? { + let webView = message.webView + return await self.encodedPlayerSettings(with: webView) } private func encodeUserValues() -> UserValues { @@ -116,14 +144,21 @@ final class DuckPlayer: DuckPlayerProtocol { } @MainActor - private func encodedSettings(with webView: WKWebView?) async -> InitialSetupSettings { + private func encodedPlayerSettings(with webView: WKWebView?) async -> InitialPlayerSettings { let isPiPEnabled = webView?.configuration.allowsPictureInPictureMediaPlayback == true - let pip = InitialSetupSettings.PIP(status: isPiPEnabled ? .enabled : .disabled) - - let playerSettings = InitialSetupSettings.PlayerSettings(pip: pip) + let pip = InitialPlayerSettings.PIP(status: isPiPEnabled ? .enabled : .disabled) + let platform = InitialPlayerSettings.Platform(name: "ios") + let environment = InitialPlayerSettings.Environment.development + let locale = InitialPlayerSettings.Locale.en + let playerSettings = InitialPlayerSettings.PlayerSettings(pip: pip) let userValues = encodeUserValues() - - return InitialSetupSettings(userValues: userValues, settings: playerSettings) + return InitialPlayerSettings(userValues: userValues, settings: playerSettings, platform: platform, locale: locale) + } + + @MainActor + private func encodedOverlaySettings(with webView: WKWebView?) async -> InitialOverlaySettings { + let userValues = encodeUserValues() + return InitialOverlaySettings(userValues: userValues) } } diff --git a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift similarity index 64% rename from DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift rename to DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift index d5288d97cd..bf93b4dd01 100644 --- a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift @@ -1,5 +1,5 @@ // -// YouTubePlayerNavigationHandler.swift +// DuckPlayerNavigationHandler.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -21,11 +21,14 @@ import Foundation import ContentScopeScripts import WebKit import Core +import Common -final class YoutubePlayerNavigationHandler { +final class DuckPlayerNavigationHandler { var duckPlayer: DuckPlayerProtocol var referrer: DuckPlayerReferrer = .other + var lastHandledVideoID: String? + var isDuckPlayerTemporarilyDisabled = false private struct Constants { static let SERPURL = "https://duckduckgo.com/" @@ -38,10 +41,13 @@ final class YoutubePlayerNavigationHandler { static let duckPlayerDefaultString = "default" static let settingsKey = "settings" static let httpMethod = "GET" + static let watchInYoutubePath = "openInYoutube" + static let watchInYoutubeVideoParameter = "v" } init(duckPlayer: DuckPlayerProtocol) { self.duckPlayer = duckPlayer + os_log("DP: Trying to load the same video while in DuckPlayer, use Youtube:", log: .duckPlayerLog, type: .debug) } static var htmlTemplatePath: String { @@ -92,14 +98,33 @@ final class YoutubePlayerNavigationHandler { } -extension YoutubePlayerNavigationHandler: DuckNavigationHandling { +extension DuckPlayerNavigationHandler: DuckNavigationHandling { // Handle rendering the simulated request if the URL is duck:// // and DuckPlayer is either enabled or alwaysAsk @MainActor - func handleNavigation(_ navigationAction: WKNavigationAction, - webView: WKWebView, - completion: @escaping (WKNavigationActionPolicy) -> Void) { + func handleNavigation(_ navigationAction: WKNavigationAction, webView: WKWebView) { + + os_log("DP: Handling DuckPlayer Player Navigation for %s", log: .duckPlayerLog, type: .debug, navigationAction.request.url?.absoluteString ?? "") + + // Handle Open in Youtube Links + // duck://player/openInYoutube?v=12345 + if let url = navigationAction.request.url, + url.scheme == "duck" { + let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + + if urlComponents?.path == "/\(Constants.watchInYoutubePath)", + let queryItems = urlComponents?.queryItems { + + if let videoParameterItem = queryItems.first(where: { $0.name == Constants.watchInYoutubeVideoParameter }), + let id = videoParameterItem.value { + // Disable DP temporarily + isDuckPlayerTemporarilyDisabled = true + handleURLChange(url: URL.youtube(id, timestamp: nil), webView: webView) + return + } + } + } // Daily Unique View Pixel if let url = navigationAction.request.url, @@ -110,8 +135,7 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { } // Pixel for Views From Youtube - if let url = navigationAction.request.url, - referrer == .youtube, + if referrer == .youtube, duckPlayer.settings.mode == .enabled { Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromYoutubeAutomatic, debounce: 2) } @@ -119,12 +143,12 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { // If DuckPlayer is Enabled or in ask mode, render the video if let url = navigationAction.request.url, url.isDuckURLScheme, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { - let html = Self.makeHTMLFromTemplate() + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk, + !isDuckPlayerTemporarilyDisabled { let newRequest = Self.makeDuckPlayerRequest(from: URLRequest(url: url)) if #available(iOS 15.0, *) { - webView.loadSimulatedRequest(newRequest, responseHTML: html) - completion(.allow) + os_log("DP: Loading Simulated Request for %s", log: .duckPlayerLog, type: .debug, navigationAction.request.url?.absoluteString ?? "") + performRequest(request: newRequest, webView: webView) return } } @@ -133,13 +157,10 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { if let url = navigationAction.request.url, let (videoID, timestamp) = url.youtubeVideoParams, duckPlayer.settings.mode == .disabled { - webView.load(URLRequest(url: URL.youtube(videoID, timestamp: timestamp))) - completion(.allow) + os_log("DP: is Disabled. We should load original video for %s", log: .duckPlayerLog, type: .debug) + handleURLChange(url: URL.youtube(videoID, timestamp: timestamp), webView: webView) return } - - completion(.allow) - } // Handle URL changes not triggered via Omnibar @@ -147,24 +168,59 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { @MainActor func handleURLChange(url: URL?, webView: WKWebView) { + // Do not handle the URL if the video was just handled + if let url = url, + url.isYoutubeVideo || url.isDuckPlayer, + let (videoID, _) = url.youtubeVideoParams, + lastHandledVideoID == videoID, + !isDuckPlayerTemporarilyDisabled { + return + } + if let url = url, url.isYoutubeVideo, !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { - webView.stopLoading() - let newURL = URL.duckPlayer(videoID, timestamp: timestamp) + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + + os_log("DP: Handling URL change: %s", log: .duckPlayerLog, type: .debug, url.absoluteString) + var newURL = URL.duckPlayer(videoID, timestamp: timestamp) + + // IF DP is temporarily disabled, load Youtube website + // Then reset the setting + if isDuckPlayerTemporarilyDisabled { + os_log("DP: Duckplayer is temporarily disabled. Opening Youtube", log: .duckPlayerLog, type: .debug) + newURL = URL.youtube(videoID, timestamp: timestamp) + } else { + os_log("DP: Duckplayer is NOT disabled. Opening DuckPlayer", log: .duckPlayerLog, type: .debug) + } + + // Load the URL webView.load(URLRequest(url: newURL)) + + // Add a short delay to let the webview start the navigation + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.lastHandledVideoID = videoID + self.isDuckPlayerTemporarilyDisabled = false + } } - } // DecidePolicyFor handler to redirect relevant requests // to duck://player @MainActor func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, - completion: @escaping (WKNavigationActionPolicy) -> Void, webView: WKWebView) { + // Do not handle the URL if the video was just handled + if let url = navigationAction.request.url, + url.isYoutubeVideo || url.isDuckPlayer, + let (videoID, _) = url.youtubeVideoParams, + lastHandledVideoID == videoID, + !isDuckPlayerTemporarilyDisabled { + return + } + + // Pixel for Views From SERP if let url = navigationAction.request.url, navigationAction.request.allHTTPHeaderFields?[Constants.refererHeader] == Constants.SERPURL, @@ -179,21 +235,20 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromOther, debounce: 2) } - if let url = navigationAction.request.url, - url.isYoutubeVideo, - !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { - webView.load(URLRequest(url: .duckPlayer(videoID, timestamp: timestamp))) - completion(.allow) + url.isYoutubeVideo, + !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + os_log("DP: Handling decidePolicy for Duck Player with %s", log: .duckPlayerLog, type: .debug, url.absoluteString) + handleURLChange(url: URL.duckPlayer(videoID, timestamp: timestamp), webView: webView) return } - completion(.allow) } // Handle Webview BackButton on DuckPlayer videos @MainActor func handleGoBack(webView: WKWebView) { + guard let backURL = webView.backForwardList.backItem?.url, backURL.isYoutubeVideo, backURL.youtubeVideoParams?.videoID == webView.url?.youtubeVideoParams?.videoID, @@ -204,14 +259,15 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { webView.goBack(skippingHistoryItems: 2) } - // Handle Reload for DuckPlayer Videos @MainActor func handleReload(webView: WKWebView) { + if let url = webView.url, url.isDuckPlayer, !url.isDuckURLScheme, let (videoID, timestamp) = url.youtubeVideoParams, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + os_log("DP: Handling DuckPlayer Reload for %s", log: .duckPlayerLog, type: .debug, url.absoluteString) webView.load(URLRequest(url: .duckPlayer(videoID, timestamp: timestamp))) } else { webView.reload() diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift b/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift index d17a9b3780..5ae878341a 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift @@ -42,8 +42,14 @@ extension URL { } static func youtube(_ videoID: String, timestamp: String? = nil) -> URL { - let url = "https://www.youtube.com/watch?v=\(videoID)".url! - return url.addingTimestamp(timestamp) + #if os(iOS) + let baseUrl = "https://m.youtube.com/watch?v=\(videoID)" + #else + let baseUrl = "https://www.youtube.com/watch?v=\(videoID)" + #endif + + let url = URL(string: baseUrl)! + return url.addingTimestamp(timestamp) } var isDuckURLScheme: Bool { diff --git a/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift index 091afb731b..261e1d912b 100644 --- a/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift @@ -107,7 +107,7 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { case Handlers.sendDuckPlayerPixel: return handleSendJSPixel case Handlers.initialSetup: - return duckPlayer.initialSetup + return duckPlayer.initialSetupOverlay default: assertionFailure("YoutubeOverlayUserScript: Failed to parse User Script message: \(methodName)") // TODO: Send pixel here diff --git a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift index 2536de5f83..af283c3615 100644 --- a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift @@ -72,7 +72,7 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { case Handlers.setUserValues: return duckPlayer.setUserValues case Handlers.initialSetup: - return duckPlayer.initialSetup + return duckPlayer.initialSetupPlayer default: assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)") return nil diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index d0a73362c6..12ed099455 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -35,6 +35,7 @@ class TabManager { private let historyManager: HistoryManaging private let syncService: DDGSyncing private var previewsSource: TabPreviewsSource + private var duckPlayerNavigationHandler: DuckNavigationHandling weak var delegate: TabDelegate? @@ -52,6 +53,9 @@ class TabManager { self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager self.syncService = syncService + + // Init Duck Player Handler + self.duckPlayerNavigationHandler = DuckPlayerNavigationHandler(duckPlayer: DuckPlayer()) registerForNotifications() } @@ -68,7 +72,8 @@ class TabManager { let controller = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) controller.applyInheritedAttribution(inheritedAttribution) controller.attachWebView(configuration: configuration, andLoadRequest: url == nil ? nil : URLRequest.userInitiated(url!), @@ -140,7 +145,8 @@ class TabManager { let controller = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) controller.attachWebView(configuration: configCopy, andLoadRequest: request, consumeCookies: !model.hasActiveTabs, diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 43d494d4e4..5f3aa279e8 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -294,7 +294,8 @@ class TabViewController: UIViewController { appSettings: AppSettings = AppDependencyProvider.shared.appSettings, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, - syncService: DDGSyncing) -> TabViewController { + syncService: DDGSyncing, + duckPlayerNavigationHandler: DuckNavigationHandling) -> TabViewController { let storyboard = UIStoryboard(name: "Tab", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "TabViewController", creator: { coder in TabViewController(coder: coder, @@ -302,7 +303,8 @@ class TabViewController: UIViewController { appSettings: appSettings, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) }) return controller } @@ -313,22 +315,22 @@ class TabViewController: UIViewController { let historyManager: HistoryManaging let historyCapture: HistoryCapture - - var duckPlayer: DuckPlayerProtocol = DuckPlayer() - var youtubeNavigationHandler: DuckNavigationHandling? + var duckPlayerNavigationHandler: DuckNavigationHandling required init?(coder aDecoder: NSCoder, tabModel: Tab, appSettings: AppSettings, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, - syncService: DDGSyncing) { + syncService: DDGSyncing, + duckPlayerNavigationHandler: DuckNavigationHandling) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager self.historyCapture = HistoryCapture(historyManager: historyManager) self.syncService = syncService + self.duckPlayerNavigationHandler = duckPlayerNavigationHandler super.init(coder: aDecoder) } @@ -346,9 +348,6 @@ class TabViewController: UIViewController { subscribeToEmailProtectionSignOutNotification() registerForDownloadsNotifications() registerForAddressBarLocationNotifications() - - // Setup DuckPlayer navigation handler - self.youtubeNavigationHandler = YoutubePlayerNavigationHandler(duckPlayer: duckPlayer) if #available(iOS 16.4, *) { registerForInspectableWebViewNotifications() @@ -681,18 +680,15 @@ class TabViewController: UIViewController { } else if let currentHost = url?.host, let newHost = webView.url?.host, currentHost == newHost { url = webView.url - if let handler = youtubeNavigationHandler, - let url, + if let url, url.isYoutubeVideo, - duckPlayer.settings.mode == .enabled { - handler.handleURLChange(url: url, webView: webView) + duckPlayerNavigationHandler.duckPlayer.settings.mode == .enabled { + duckPlayerNavigationHandler.handleURLChange(url: url, webView: webView) } } - if var handler = youtubeNavigationHandler, - let url { - handler.referrer = url.isYoutube ? .youtube : .other - + if let url { + duckPlayerNavigationHandler.referrer = url.isYoutube ? .youtube : .other } } @@ -744,8 +740,8 @@ class TabViewController: UIViewController { public func reload() { updateContentMode() cachedRuntimeConfigurationForDomain = [:] - if let url = webView.url, url.isDuckPlayer, let handler = youtubeNavigationHandler { - handler.handleReload(webView: webView) + if let url = webView.url, url.isDuckPlayer { + duckPlayerNavigationHandler.handleReload(webView: webView) } else { webView.reload() } @@ -759,8 +755,8 @@ class TabViewController: UIViewController { func goBack() { dismissJSAlertIfNeeded() - if let url = url, url.isDuckPlayer, let handler = youtubeNavigationHandler { - handler.handleGoBack(webView: webView) + if let url = url, url.isDuckPlayer { + duckPlayerNavigationHandler.handleGoBack(webView: webView) chromeDelegate?.omniBar.resignFirstResponder() return } @@ -1676,10 +1672,10 @@ extension TabViewController: WKNavigationDelegate { } if navigationAction.isTargetingMainFrame(), - let handler = youtubeNavigationHandler, url.isYoutubeVideo, - duckPlayer.settings.mode == .enabled { - handler.handleDecidePolicyFor(navigationAction, completion: completion, webView: webView) + duckPlayerNavigationHandler.duckPlayer.settings.mode == .enabled { + duckPlayerNavigationHandler.handleDecidePolicyFor(navigationAction, webView: webView) + completion(.allow) return } @@ -1700,11 +1696,9 @@ extension TabViewController: WKNavigationDelegate { performBlobNavigation(navigationAction, completion: completion) case .duck: - if let handler = youtubeNavigationHandler { - handler.handleNavigation(navigationAction, webView: webView, completion: completion) - return - } + duckPlayerNavigationHandler.handleNavigation(navigationAction, webView: webView) completion(.cancel) + return case .unknown: if navigationAction.navigationType == .linkActivated { @@ -2353,7 +2347,7 @@ extension TabViewController: UserContentControllerDelegate { userScripts.autoconsentUserScript.delegate = self // Setup DuckPlayer - userScripts.duckPlayer = duckPlayer + userScripts.duckPlayer = duckPlayerNavigationHandler.duckPlayer userScripts.youtubeOverlayScript?.webView = webView userScripts.youtubePlayerUserScript?.webView = webView diff --git a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index e6a48170cd..b25fb38e35 100644 --- a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -105,7 +105,8 @@ extension TabViewController { let tabController = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) tabController.isLinkPreview = true let configuration = WKWebViewConfiguration.nonPersistent() tabController.attachWebView(configuration: configuration, andLoadRequest: URLRequest.userInitiated(url), consumeCookies: false) diff --git a/DuckDuckGoTests/DuckPlayerMocks.swift b/DuckDuckGoTests/DuckPlayerMocks.swift index aea76a748a..ed84be6f1d 100644 --- a/DuckDuckGoTests/DuckPlayerMocks.swift +++ b/DuckDuckGoTests/DuckPlayerMocks.swift @@ -110,6 +110,14 @@ final class MockDuckPlayerSettings: DuckPlayerSettingsProtocol { } final class MockDuckPlayer: DuckPlayerProtocol { + func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + + func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + var settings: any DuckPlayerSettingsProtocol init(settings: DuckPlayerSettingsProtocol) { diff --git a/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift b/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift index d5df8b9603..d45e6ca8f7 100644 --- a/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift +++ b/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift @@ -22,6 +22,12 @@ import os.log @testable import DuckDuckGo final class DuckPlayerURLExtensionTests: XCTestCase { + + #if os(iOS) + let baseUrl = "https://m.youtube.com" + #else + let baseUrl = "https://www.youtube.com" + #endif func testIsDuckPlayerScheme() { XCTAssertTrue("duck:player/abcdef12345".url!.isDuckURLScheme) @@ -29,7 +35,7 @@ final class DuckPlayerURLExtensionTests: XCTestCase { XCTAssertTrue("duck://player/abcdef".url!.isDuckURLScheme) XCTAssertTrue("duck://player/12345".url!.isDuckURLScheme) XCTAssertFalse("http://duckplayer/abcdef12345".url!.isDuckURLScheme) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345".url!.isDuckURLScheme) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345".url!.isDuckURLScheme) XCTAssertFalse("https://www.youtube-nocookie.com/embed/abcdef12345".url!.isDuckURLScheme) } @@ -41,25 +47,25 @@ final class DuckPlayerURLExtensionTests: XCTestCase { XCTAssertFalse("https://www.youtube-nocookie.com/embed?t=23s".url!.isDuckPlayer) XCTAssertTrue("duck://player/abcdef12345".url!.isDuckPlayer) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345".url!.isDuckPlayer) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345".url!.isDuckPlayer) XCTAssertFalse("https://duckduckgo.com".url!.isDuckPlayer) } func testIsYoutubePlaylist() { - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubePlaylist) - XCTAssertTrue("https://www.youtube.com/watch?list=abcdefgh12345678&v=abcdef12345".url!.isYoutubePlaylist) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubePlaylist) + XCTAssertTrue("\(baseUrl)/watch?list=abcdefgh12345678&v=abcdef12345".url!.isYoutubePlaylist) XCTAssertFalse("https://duckduckgo.com/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubePlaylist) - XCTAssertFalse("https://www.youtube.com/watch?list=abcdefgh12345678".url!.isYoutubePlaylist) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubePlaylist) + XCTAssertFalse("\(baseUrl)/watch?list=abcdefgh12345678".url!.isYoutubePlaylist) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubePlaylist) } func testIsYoutubeVideo() { - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345".url!.isYoutubeVideo) - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubeVideo) - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345&t=5m".url!.isYoutubeVideo) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345".url!.isYoutubeVideo) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubeVideo) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345&t=5m".url!.isYoutubeVideo) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubeVideo) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubeVideo) XCTAssertFalse("https://duckduckgo.com/watch?v=abcdef12345".url!.isYoutubeVideo) } @@ -74,15 +80,15 @@ final class DuckPlayerURLExtensionTests: XCTestCase { } func testYoutubeVideoParamsFromYoutubeURL() { - let params = "https://www.youtube.com/watch?v=abcdef12345".url!.youtubeVideoParams + let params = "\(baseUrl)/watch?v=abcdef12345".url!.youtubeVideoParams XCTAssertEqual(params?.videoID, "abcdef12345") XCTAssertEqual(params?.timestamp, nil) - let paramsWithTimestamp = "https://www.youtube.com/watch?v=abcdef12345&t=23s".url!.youtubeVideoParams + let paramsWithTimestamp = "\(baseUrl)/watch?v=abcdef12345&t=23s".url!.youtubeVideoParams XCTAssertEqual(paramsWithTimestamp?.videoID, "abcdef12345") XCTAssertEqual(paramsWithTimestamp?.timestamp, "23s") - let paramsWithTimestampWithoutUnits = "https://www.youtube.com/watch?t=102&v=abcdef12345&feature=youtu.be".url!.youtubeVideoParams + let paramsWithTimestampWithoutUnits = "\(baseUrl)/watch?t=102&v=abcdef12345&feature=youtu.be".url!.youtubeVideoParams XCTAssertEqual(paramsWithTimestampWithoutUnits?.videoID, "abcdef12345") XCTAssertEqual(paramsWithTimestampWithoutUnits?.timestamp, "102") } @@ -110,15 +116,15 @@ final class DuckPlayerURLExtensionTests: XCTestCase { } func testYoutubeURLTimestampValidation() { - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: nil).absoluteString, "https://www.youtube.com/watch?v=abcdef12345") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "23s").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=23s") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5s").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=5m5s") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h400m100s").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=12h400m100s") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h2s2h").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=12h2s2h") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5m5m").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=5m5m5m") - - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=5") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "10d").absoluteString, "https://www.youtube.com/watch?v=abcdef12345") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: nil).absoluteString, "\(baseUrl)/watch?v=abcdef12345") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "23s").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=23s") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5s").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=5m5s") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h400m100s").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=12h400m100s") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h2s2h").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=12h2s2h") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5m5m").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=5m5m5m") + + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=5") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "10d").absoluteString, "\(baseUrl)/watch?v=abcdef12345") } func testYoutubeNoCookieURLTimestampValidation() { diff --git a/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift b/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift index fe4f76eb09..877980dc8e 100644 --- a/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift +++ b/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift @@ -25,14 +25,14 @@ import BrowserServicesKit @testable import DuckDuckGo -class YoutubePlayerNavigationHandlerTests: XCTestCase { +class DuckPlayerNavigationHandlerTests: XCTestCase { var webView: WKWebView! var mockWebView: MockWebView! var mockNavigationDelegate: MockWKNavigationDelegate! var mockAppSettings: AppSettingsMock! var mockPrivacyConfig: PrivacyConfigurationManagerMock! - + override func setUp() { super.setUp() webView = WKWebView() @@ -52,7 +52,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { // Test for htmlTemplatePath existence func testHtmlTemplatePathExists() { - let templatePath = YoutubePlayerNavigationHandler.htmlTemplatePath + let templatePath = DuckPlayerNavigationHandler.htmlTemplatePath let fileExists = FileManager.default.fileExists(atPath: templatePath) XCTAssertFalse(templatePath.isEmpty, "The template path should not be empty") XCTAssertTrue(fileExists, "The template file should exist at the specified path") @@ -62,7 +62,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { func testMakeDuckPlayerRequestFromOriginalRequest() { let originalRequest = URLRequest(url: URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")!) - let duckPlayerRequest = YoutubePlayerNavigationHandler.makeDuckPlayerRequest(from: originalRequest) + let duckPlayerRequest = DuckPlayerNavigationHandler.makeDuckPlayerRequest(from: originalRequest) XCTAssertEqual(duckPlayerRequest.url?.host, "www.youtube-nocookie.com") XCTAssertEqual(duckPlayerRequest.url?.path, "/embed/abc123") @@ -76,7 +76,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let videoID = "abc123" let timestamp = "10s" - let duckPlayerRequest = YoutubePlayerNavigationHandler.makeDuckPlayerRequest(for: videoID, timestamp: timestamp) + let duckPlayerRequest = DuckPlayerNavigationHandler.makeDuckPlayerRequest(for: videoID, timestamp: timestamp) XCTAssertEqual(duckPlayerRequest.url?.host, "www.youtube-nocookie.com") XCTAssertEqual(duckPlayerRequest.url?.path, "/embed/abc123") @@ -87,24 +87,23 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { // Test for makeHTMLFromTemplate func testMakeHTMLFromTemplate() { - let expectedHtml = try? String(contentsOfFile: YoutubePlayerNavigationHandler.htmlTemplatePath) - let html = YoutubePlayerNavigationHandler.makeHTMLFromTemplate() + let expectedHtml = try? String(contentsOfFile: DuckPlayerNavigationHandler.htmlTemplatePath) + let html = DuckPlayerNavigationHandler.makeHTMLFromTemplate() XCTAssertEqual(html, expectedHtml) } - // Test for handleURLChange + // MARK: handleURLChange tests @MainActor func testHandleURLChangeDuckPlayerEnabled() { let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleURLChange(url: youtubeURL, webView: mockWebView) - XCTAssertTrue(mockWebView.didStopLoadingCalled, "Expected stopLoading to be called") XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") if let loadedRequest = mockWebView.lastLoadedRequest { @@ -113,6 +112,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { XCTAssertEqual(loadedRequest.url?.path, "/abc123") XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) } + } @MainActor @@ -122,157 +122,147 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleURLChange(url: youtubeURL, webView: mockWebView) - XCTAssertFalse(mockWebView.didStopLoadingCalled, "Expected stopLoading Not to be called") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request NOT to be loaded") } @MainActor - func testHandleURLChangeForNonYouTubeVideo() { + func testHandleURLChangeDuckPlayerTemporarilyDisabled() { + let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! + + let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) + playerSettings.setMode(.enabled) + let player = MockDuckPlayer(settings: playerSettings) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + + handler.isDuckPlayerTemporarilyDisabled = true + + handler.handleURLChange(url: youtubeURL, webView: mockWebView) + + XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") + + if let loadedRequest = mockWebView.lastLoadedRequest { + XCTAssertEqual(loadedRequest.url?.scheme, "https") + XCTAssertEqual(loadedRequest.url?.host, "m.youtube.com") + XCTAssertEqual(loadedRequest.url?.path, "/watch") + XCTAssertEqual(loadedRequest.url?.query?.contains("v=abc123"), true) + XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) + } + } + + @MainActor + func testHandleURLChangeNonYouTubeURL() { let nonYouTubeURL = URL(string: "https://www.google.com")! let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.disabled) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleURLChange(url: nonYouTubeURL, webView: mockWebView) - XCTAssertFalse(mockWebView.didStopLoadingCalled, "Expected stopLoading not to be called") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request NOT to be loaded") } - // Test for handleDecidePolicyFor @MainActor - func testHandleDecidePolicyForWithDuckPlayerEnabled() { - let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! - let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? + func testHandleNavigationOpenInYoutubeLink() { + let duckURL = URL(string: "duck://player/openInYoutube?v=12345")! + let navigationAction = MockNavigationAction(request: URLRequest(url: duckURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy - expectation.fulfill() - }, webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) - waitForExpectations(timeout: 1, handler: nil) + handler.handleNavigation(navigationAction, webView: mockWebView) - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") + XCTAssertTrue(handler.isDuckPlayerTemporarilyDisabled, "Expected DuckPlayer to be temporarily disabled") XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") if let loadedRequest = mockWebView.lastLoadedRequest { - XCTAssertEqual(loadedRequest.url?.scheme, "duck") - XCTAssertEqual(loadedRequest.url?.host, "player") - XCTAssertEqual(loadedRequest.url?.path, "/abc123") - XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) + XCTAssertEqual(loadedRequest.url?.scheme, "https") + XCTAssertEqual(loadedRequest.url?.host, "m.youtube.com") + XCTAssertEqual(loadedRequest.url?.path, "/watch") + XCTAssertEqual(loadedRequest.url?.query?.contains("v=12345"), true) } - } @MainActor - func testHandleDecidePolicyForWithDuckPlayerDisabled() { - let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! - let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? + func testHandleNavigationDuckPlayerEnabledAlreadyInDuckPlayer() { + let duckPlayerURL = URL(string: "duck://player/CYTASDSD")! + let navigationAction = MockNavigationAction(request: URLRequest(url: duckPlayerURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + player.settings.setMode(.enabled) - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy - expectation.fulfill() - }, webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + handler.handleNavigation(navigationAction, webView: mockWebView) - waitForExpectations(timeout: 1, handler: nil) - - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") - + } + @MainActor - func testHandleDecidePolicyForNonYouTubeVideoWithDuckPlayerEnabled() { - let nonYouTubeURL = URL(string: "https://www.google.com")! - let navigationAction = MockNavigationAction(request: URLRequest(url: nonYouTubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? + func testHandleNavigationDuckPlayerDisabled() { + let duckPlayerURL = URL(string: "duck://player/CUIUIIUI")! + let navigationAction = MockNavigationAction(request: URLRequest(url: duckPlayerURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy - expectation.fulfill() - }, webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + + handler.handleNavigation(navigationAction, webView: mockWebView) - waitForExpectations(timeout: 1, handler: nil) + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") } @MainActor - func testHandleDecidePolicyForNonYouTubeVideoWithDuckPlayerDisabled() { - let nonYouTubeURL = URL(string: "https://www.google.com")! - let navigationAction = MockNavigationAction(request: URLRequest(url: nonYouTubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? - + func testHandleDecidePolicyForVideoJustHandled() { + let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! + let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.disabled) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + + // Call handleDecidePolicyFor twice with the same URL to simulate handling the same video twice + handler.handleDecidePolicyFor(navigationAction, webView: mockWebView) + + // Wait for 0.8 seconds to simulate the time delay + let expectation = self.expectation(description: "Wait for 0.8 seconds") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + handler.handleDecidePolicyFor(navigationAction, webView: self.mockWebView) expectation.fulfill() - }, webView: mockWebView) + } - waitForExpectations(timeout: 1, handler: nil) + waitForExpectations(timeout: 1.0, handler: nil) - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") + // Verify that the second call did not load a new request + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected no new request to be loaded since video was just handled") } @MainActor - func testHandleReloadForDuckPlayerVideoWithDuckPlayerEnabled() { - let duckPlayerURL = URL(string: "https://www.youtube-nocookie.com/embed/abc123?t=10s")! - - mockWebView.setCurrentURL(duckPlayerURL) + func testHandleDecidePolicyForTransformYoutubeURL() { + let youtubeURL = URL(string: "https://m.youtube.com/watch?v=abc123&t=10s")! + let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleReload(webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) - XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") + handler.handleDecidePolicyFor(navigationAction, webView: mockWebView) - if let loadedRequest = mockWebView.lastLoadedRequest { - XCTAssertEqual(loadedRequest.url?.scheme, "duck") - XCTAssertEqual(loadedRequest.url?.host, "player") - XCTAssertEqual(loadedRequest.url?.path, "/abc123") - XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) - } + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") + } @MainActor @@ -284,14 +274,14 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleReload(webView: mockWebView) XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") } - + @MainActor func testHandleReloadForNonDuckPlayerVideoWithDuckPlayerEnabled() { let nonDuckPlayerURL = URL(string: "https://www.google.com")! @@ -302,7 +292,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.alwaysAsk) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleReload(webView: mockWebView) XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") @@ -318,10 +308,10 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleReload(webView: mockWebView) XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") } - + } diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index afb4f6128a..a603ff9af2 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit afb4f6128a3b50d53ddcb1897ea1fb4df6858aa1 +Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8 From bc098bb8176f3e728ee15cbbd0d37d946a6b6a98 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 19 Jul 2024 18:37:05 +0200 Subject: [PATCH 46/48] [DuckPlayer] 7- Open Settings (#3110) Task/Issue URL: https://app.asana.com/0/1204099484721401/1207821841500245/f Description: Tapping on the "Settings" button, open app settings in DuckPlayer Tapping on the "Info" button, opens DuckPlayer info sheet --- DuckDuckGo/DuckPlayer/DuckPlayer.swift | 32 +++++++++++++++++++ .../DuckPlayer/YoutubePlayerUserScript.swift | 6 ++++ DuckDuckGo/MainViewController.swift | 19 +++++++++++ DuckDuckGo/SettingsRootView.swift | 2 ++ DuckDuckGo/SettingsViewModel.swift | 7 ++++ DuckDuckGoTests/DuckPlayerMocks.swift | 12 +++++++ 6 files changed, 78 insertions(+) diff --git a/DuckDuckGo/DuckPlayer/DuckPlayer.swift b/DuckDuckGo/DuckPlayer/DuckPlayer.swift index df7e5ecf29..4192367a0d 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayer.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayer.swift @@ -81,15 +81,20 @@ public enum DuckPlayerReferrer { protocol DuckPlayerProtocol { var settings: DuckPlayerSettingsProtocol { get } + var hostView: UIViewController? { get } init(settings: DuckPlayerSettingsProtocol) func setUserValues(params: Any, message: WKScriptMessage) -> Encodable? func getUserValues(params: Any, message: WKScriptMessage) -> Encodable? func openVideoInDuckPlayer(url: URL, webView: WKWebView) + func openDuckPlayerSettings(params: Any, message: WKScriptMessage) async -> Encodable? + func openDuckPlayerInfo(params: Any, message: WKScriptMessage) async -> Encodable? func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> Encodable? func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> Encodable? + + func setHostViewController(_ vc: UIViewController) } final class DuckPlayer: DuckPlayerProtocol { @@ -98,11 +103,18 @@ final class DuckPlayer: DuckPlayerProtocol { static let commonName = "Duck Player" private(set) var settings: DuckPlayerSettingsProtocol + private(set) var hostView: UIViewController? init(settings: DuckPlayerSettingsProtocol = DuckPlayerSettings()) { self.settings = settings } + // Sets a presenting VC, so DuckPlayer can present the + // info sheet directly + public func setHostViewController(_ vc: UIViewController) { + hostView = vc + } + // MARK: - Common Message Handlers public func setUserValues(params: Any, message: WKScriptMessage) -> Encodable? { @@ -135,6 +147,26 @@ final class DuckPlayer: DuckPlayerProtocol { let webView = message.webView return await self.encodedPlayerSettings(with: webView) } + + public func openDuckPlayerSettings(params: Any, message: WKScriptMessage) async -> Encodable? { + NotificationCenter.default.post( + name: .settingsDeepLinkNotification, + object: SettingsViewModel.SettingsDeepLinkSection.duckPlayer, + userInfo: nil + ) + return nil + } + + @MainActor + public func presentDuckPlayerInfo() { + guard let hostView else { return } + DuckPlayerModalPresenter().presentDuckPlayerFeatureModal(on: hostView) + } + + public func openDuckPlayerInfo(params: Any, message: WKScriptMessage) async -> Encodable? { + await presentDuckPlayerInfo() + return nil + } private func encodeUserValues() -> UserValues { UserValues( diff --git a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift index af283c3615..e85f0ee19b 100644 --- a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift @@ -35,6 +35,8 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { static let setUserValues = "setUserValues" static let getUserValues = "getUserValues" static let initialSetup = "initialSetup" + static let openSettings = "openSettings" + static let openInfo = "openInfo" } init(duckPlayer: DuckPlayerProtocol) { @@ -73,6 +75,10 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { return duckPlayer.setUserValues case Handlers.initialSetup: return duckPlayer.initialSetupPlayer + case Handlers.openSettings: + return duckPlayer.openDuckPlayerSettings + case Handlers.openInfo: + return duckPlayer.openDuckPlayerInfo default: assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)") return nil diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 4399ff1653..51675e1d04 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -113,6 +113,7 @@ class MainViewController: UIViewController { private var favoritesDisplayModeCancellable: AnyCancellable? private var emailCancellables = Set() private var urlInterceptorCancellables = Set() + private var settingsDeepLinkcancellables = Set() #if NETWORK_PROTECTION private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults @@ -261,6 +262,7 @@ class MainViewController: UIViewController { addLaunchTabNotificationObserver() subscribeToEmailProtectionStatusNotifications() subscribeToURLInterceptorNotifications() + subscribeToSettingsDeeplinkNotifications() #if NETWORK_PROTECTION subscribeToNetworkProtectionEvents() @@ -1349,6 +1351,23 @@ class MainViewController: UIViewController { } .store(in: &urlInterceptorCancellables) } + + private func subscribeToSettingsDeeplinkNotifications() { + NotificationCenter.default.publisher(for: .settingsDeepLinkNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] notification in + switch notification.object as? SettingsViewModel.SettingsDeepLinkSection { + + case .duckPlayer: + let deepLinkTarget: SettingsViewModel.SettingsDeepLinkSection + deepLinkTarget = .duckPlayer + self?.launchSettings(deepLinkTarget: deepLinkTarget) + default: + return + } + } + .store(in: &settingsDeepLinkcancellables) + } #if NETWORK_PROTECTION private func subscribeToNetworkProtectionEvents() { diff --git a/DuckDuckGo/SettingsRootView.swift b/DuckDuckGo/SettingsRootView.swift index 32d4599313..415ce3d3bd 100644 --- a/DuckDuckGo/SettingsRootView.swift +++ b/DuckDuckGo/SettingsRootView.swift @@ -117,6 +117,8 @@ struct SettingsRootView: View { SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, navigationCoordinator: subscriptionNavigationCoordinator, subscriptionManager: AppDependencyProvider.shared.subscriptionManager) + case .duckPlayer: + SettingsDuckPlayerView().environmentObject(viewModel) default: EmptyView() } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index f437dd7d4f..a31df34ba0 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -634,6 +634,7 @@ extension SettingsViewModel { case dbp case itr case subscriptionFlow(origin: String? = nil) + case duckPlayer // Add other cases as needed var id: String { @@ -642,6 +643,7 @@ extension SettingsViewModel { case .dbp: return "dbp" case .itr: return "itr" case .subscriptionFlow: return "subscriptionFlow" + case .duckPlayer: return "duckPlayer" // Ensure all cases are covered } } @@ -791,3 +793,8 @@ extension SettingsViewModel { } } + +// Deeplink notification handling +extension NSNotification.Name { + static let settingsDeepLinkNotification: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.notification.settingsDeepLink") +} diff --git a/DuckDuckGoTests/DuckPlayerMocks.swift b/DuckDuckGoTests/DuckPlayerMocks.swift index ed84be6f1d..09d95354a8 100644 --- a/DuckDuckGoTests/DuckPlayerMocks.swift +++ b/DuckDuckGoTests/DuckPlayerMocks.swift @@ -110,6 +110,18 @@ final class MockDuckPlayerSettings: DuckPlayerSettingsProtocol { } final class MockDuckPlayer: DuckPlayerProtocol { + var hostView: UIViewController? + + func openDuckPlayerSettings(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + + func openDuckPlayerInfo(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + + func setHostViewController(_ vc: UIViewController) {} + func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> (any Encodable)? { nil } From 4486729cf94fcd297591326aeba2c139ff8a3566 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 19 Jul 2024 09:40:41 -0700 Subject: [PATCH 47/48] Update BSK for Mac RMF changes (#3107) Task/Issue URL: https://app.asana.com/0/1199333091098016/1207851439577905/f Tech Design URL: CC: Description: This PR updates BSK for Mac RMF changes. iOS is not affected. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cc96638f15..7b92881927 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10182,7 +10182,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 172.0.1; + version = 173.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a8a4f9f886..5720e2d319 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "3274feb8d84fda5f27541c13f2ab428b4e77a5e2", - "version" : "172.0.1" + "revision" : "16686ec1d3a8641b47c55d5271e29c7dbe4c9e73", + "version" : "173.0.0" } }, { From ba71738dcb56dc8a370b4aa0cde72bdbb4cbbd9d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 19 Jul 2024 10:34:11 -0700 Subject: [PATCH 48/48] Reduce VPN manager instances (#3097) Task/Issue URL: https://app.asana.com/0/72649045549333/1207151621945908/f Tech Design URL: CC: Description: This PR reduces the number of VPN manager instances that we create through the lifetime of the app. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- DuckDuckGo/AppDependencyProvider.swift | 2 + ...orkProtectionConvenienceInitialisers.swift | 2 +- .../NetworkProtectionDebugUtilities.swift | 8 +- DuckDuckGo/NetworkProtectionRootView.swift | 1 + DuckDuckGo/NetworkProtectionStatusView.swift | 8 +- .../NetworkProtectionStatusViewModel.swift | 67 +++++----- .../NetworkProtectionTunnelController.swift | 115 +++++++++++------- DuckDuckGoTests/MockDependencyProvider.swift | 2 + 10 files changed, 125 insertions(+), 88 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7b92881927..5131c32908 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10182,7 +10182,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 173.0.0; + version = 174.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5720e2d319..cfa33a3392 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "16686ec1d3a8641b47c55d5271e29c7dbe4c9e73", - "version" : "173.0.0" + "revision" : "6db80afec11da4f0a36a81dc6030f7e83a524c87", + "version" : "174.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 228093422b..fea06bf74e 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -47,6 +47,7 @@ protocol DependencyProvider { var networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore { get } var networkProtectionTunnelController: NetworkProtectionTunnelController { get } var connectionObserver: ConnectionStatusObserver { get } + var serverInfoObserver: ConnectionServerInfoObserver { get } var vpnSettings: VPNSettings { get } } @@ -88,6 +89,7 @@ class AppDependencyProvider: DependencyProvider { let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) let connectionObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession() + let serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession() let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) init() { diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index cfa8429d58..0840e6fe1b 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -28,7 +28,7 @@ import Subscription private class DefaultTunnelSessionProvider: TunnelSessionProvider { func activeSession() async -> NETunnelProviderSession? { - try? await ConnectionSessionUtilities.activeSession() + return await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() } } diff --git a/DuckDuckGo/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtectionDebugUtilities.swift index 22a16a2b83..0186856979 100644 --- a/DuckDuckGo/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtectionDebugUtilities.swift @@ -31,7 +31,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Registation Key func expireRegistrationKeyNow() async { - guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() else { return } @@ -41,7 +41,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Notifications func sendTestNotificationRequest() async throws { - guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() else { return } @@ -51,7 +51,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Disable VPN func disableConnectOnDemandAndShutDown() async { - guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() else { return } @@ -61,7 +61,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Failure Simulation func triggerSimulation(_ option: NetworkProtectionSimulationOption) async { - guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() else { return } diff --git a/DuckDuckGo/NetworkProtectionRootView.swift b/DuckDuckGo/NetworkProtectionRootView.swift index bdad605a52..1d23f301a9 100644 --- a/DuckDuckGo/NetworkProtectionRootView.swift +++ b/DuckDuckGo/NetworkProtectionRootView.swift @@ -33,6 +33,7 @@ struct NetworkProtectionRootView: View { statusViewModel = NetworkProtectionStatusViewModel(tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController, settings: AppDependencyProvider.shared.vpnSettings, statusObserver: AppDependencyProvider.shared.connectionObserver, + serverInfoObserver: AppDependencyProvider.shared.serverInfoObserver, locationListRepository: locationListRepository) } var body: some View { diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index 67abeb033a..b98cc348da 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -202,11 +202,9 @@ struct NetworkProtectionStatusView: View { @ViewBuilder private func about() -> some View { Section { - if statusModel.shouldShowFAQ { - NavigationLink(UserText.netPVPNSettingsFAQ, destination: LazyView(NetworkProtectionFAQView())) - .daxBodyRegular() - .foregroundColor(.init(designSystemColor: .textPrimary)) - } + NavigationLink(UserText.netPVPNSettingsFAQ, destination: LazyView(NetworkProtectionFAQView())) + .daxBodyRegular() + .foregroundColor(.init(designSystemColor: .textPrimary)) NavigationLink(UserText.netPVPNSettingsShareFeedback, destination: VPNFeedbackFormCategoryView()) .daxBodyRegular() diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index 2509a21e73..8419921d0d 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -87,7 +87,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject { return formatter }() - private let tunnelController: TunnelController + private let tunnelController: (TunnelController & TunnelSessionProvider) private let statusObserver: ConnectionStatusObserver private let serverInfoObserver: ConnectionServerInfoObserver private let errorObserver: ConnectionErrorObserver @@ -134,16 +134,12 @@ final class NetworkProtectionStatusViewModel: ObservableObject { @Published public var downloadTotal: String? private var throughputUpdateTimer: Timer? - var shouldShowFAQ: Bool { - AppDependencyProvider.shared.subscriptionFeatureAvailability.isFeatureAvailable - } - @Published public var animationsOn: Bool = false - public init(tunnelController: TunnelController, + public init(tunnelController: (TunnelController & TunnelSessionProvider), settings: VPNSettings, statusObserver: ConnectionStatusObserver, - serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession(), + serverInfoObserver: ConnectionServerInfoObserver, errorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession(), locationListRepository: NetworkProtectionLocationListRepository) { self.tunnelController = tunnelController @@ -159,6 +155,8 @@ final class NetworkProtectionStatusViewModel: ObservableObject { self.dnsSettings = settings.dnsSettings + updateViewModel(withStatus: statusObserver.recentValue) + setUpIsConnectedStatePublishers() setUpToggledStatePublisher() setUpStatusMessagePublishers() @@ -176,30 +174,10 @@ final class NetworkProtectionStatusViewModel: ObservableObject { } private func setUpIsConnectedStatePublishers() { - let isConnectedPublisher = statusObserver.publisher - .map { $0.isConnected } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - isConnectedPublisher - .map(Self.titleText(connected:)) - .assign(to: \.headerTitle, onWeaklyHeld: self) - .store(in: &cancellables) - isConnectedPublisher - .map(Self.statusImageID(connected:)) - .assign(to: \.statusImageID, onWeaklyHeld: self) - .store(in: &cancellables) - isConnectedPublisher - .sink { [weak self] isConnected in - if !isConnected { - self?.uploadTotal = nil - self?.downloadTotal = nil - self?.throughputUpdateTimer?.invalidate() - self?.throughputUpdateTimer = nil - } else { - self?.setUpThroughputRefreshTimer() - } - } - .store(in: &cancellables) + statusObserver.publisher.sink { [weak self] status in + self?.updateViewModel(withStatus: status) + } + .store(in: &cancellables) } private func setUpToggledStatePublisher() { @@ -292,6 +270,31 @@ final class NetworkProtectionStatusViewModel: ObservableObject { .store(in: &cancellables) } + private func updateViewModel(withStatus connectionStatus: ConnectionStatus) { + self.headerTitle = Self.titleText(connected: connectionStatus.isConnected) + self.statusImageID = Self.statusImageID(connected: connectionStatus.isConnected) + + if !connectionStatus.isConnected { + self.uploadTotal = nil + self.downloadTotal = nil + self.throughputUpdateTimer?.invalidate() + self.throughputUpdateTimer = nil + } else { + self.setUpThroughputRefreshTimer() + } + + switch connectionStatus { + case .connected: + self.isNetPEnabled = true + case .connecting: + self.isNetPEnabled = true + self.resetConnectionInformation() + default: + self.isNetPEnabled = false + self.resetConnectionInformation() + } + } + private func setUpErrorPublishers() { guard AppDependencyProvider.shared.internalUserDecider.isInternalUser else { return @@ -346,7 +349,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject { } private func refreshDataVolumeTotals() async { - guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + guard let activeSession = await tunnelController.activeSession() else { return } diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index 02f5953de7..30ef1e6f80 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -33,9 +33,10 @@ enum VPNConfigurationRemovalReason: String { case debugMenu } -final class NetworkProtectionTunnelController: TunnelController { +final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { static var shouldSimulateFailure: Bool = false + private var internalManager: NETunnelProviderManager? private let debugFeatures = NetworkProtectionDebugFeatures() private let tokenStore: NetworkProtectionKeychainTokenStore private let errorStore = NetworkProtectionTunnelErrorStore() @@ -43,6 +44,44 @@ final class NetworkProtectionTunnelController: TunnelController { private var previousStatus: NEVPNStatus = .invalid private var cancellables = Set() + // MARK: - Manager, Session, & Connection + + /// The tunnel manager: will try to load if it its not loaded yet, but if one can't be loaded from preferences, + /// a new one will not be created. This is useful for querying the connection state and information without triggering + /// a VPN-access popup to the user. + /// + @MainActor var tunnelManager: NETunnelProviderManager? { + get async { + if let internalManager { + return internalManager + } + + let loadedManager = try? await NETunnelProviderManager.loadAllFromPreferences().first + internalManager = loadedManager + return loadedManager + } + } + + public var connection: NEVPNConnection? { + get async { + await tunnelManager?.connection + } + } + + public func activeSession() async -> NETunnelProviderSession? { + await session + } + + public var session: NETunnelProviderSession? { + get async { + guard let manager = await tunnelManager, let session = manager.connection as? NETunnelProviderSession else { + return nil + } + + return session + } + } + // MARK: - Starting & Stopping the VPN enum StartError: LocalizedError, CustomNSError { @@ -83,6 +122,7 @@ final class NetworkProtectionTunnelController: TunnelController { init(accountManager: AccountManager, tokenStore: NetworkProtectionKeychainTokenStore) { self.tokenStore = tokenStore subscribeToStatusChanges() + subscribeToConfigurationChanges() } /// Starts the VPN connection used for Network Protection @@ -106,7 +146,7 @@ final class NetworkProtectionTunnelController: TunnelController { } func stop() async { - guard let tunnelManager = await loadTunnelManager() else { + guard let tunnelManager = await self.tunnelManager else { return } @@ -139,8 +179,7 @@ final class NetworkProtectionTunnelController: TunnelController { var isInstalled: Bool { get async { - let tunnelManager = await loadTunnelManager() - return tunnelManager != nil + return await self.tunnelManager != nil } } @@ -150,7 +189,7 @@ final class NetworkProtectionTunnelController: TunnelController { /// var isConnected: Bool { get async { - guard let tunnelManager = await loadTunnelManager() else { + guard let tunnelManager = await self.tunnelManager else { return false } @@ -174,7 +213,7 @@ final class NetworkProtectionTunnelController: TunnelController { switch tunnelManager.connection.status { case .invalid: - reloadTunnelManager() + clearInternalManager() try await startWithError() case .connected: // Intentional no-op @@ -184,10 +223,8 @@ final class NetworkProtectionTunnelController: TunnelController { } } - /// Reloads the tunnel manager from preferences. - /// - private func reloadTunnelManager() { - internalTunnelManager = nil + private func clearInternalManager() { + internalManager = nil } private func start(_ tunnelManager: NETunnelProviderManager) throws { @@ -224,35 +261,11 @@ final class NetworkProtectionTunnelController: TunnelController { } } - /// The actual storage for our tunnel manager. - /// - private var internalTunnelManager: NETunnelProviderManager? - - /// The tunnel manager: will try to load if it its not loaded yet, but if one can't be loaded from preferences, - /// a new one will not be created. This is useful for querying the connection state and information without triggering - /// a VPN-access popup to the user. - /// - private var tunnelManager: NETunnelProviderManager? { - get async { - guard let tunnelManager = internalTunnelManager else { - let tunnelManager = await loadTunnelManager() - internalTunnelManager = tunnelManager - return tunnelManager - } - - return tunnelManager - } - } - - private func loadTunnelManager() async -> NETunnelProviderManager? { - try? await NETunnelProviderManager.loadAllFromPreferences().first - } - private func loadOrMakeTunnelManager() async throws -> NETunnelProviderManager { guard let tunnelManager = await tunnelManager else { let tunnelManager = NETunnelProviderManager() try await setupAndSave(tunnelManager) - internalTunnelManager = tunnelManager + internalManager = tunnelManager return tunnelManager } @@ -262,12 +275,7 @@ final class NetworkProtectionTunnelController: TunnelController { private func setupAndSave(_ tunnelManager: NETunnelProviderManager) async throws { setup(tunnelManager) - try await saveToPreferences(tunnelManager) - try await loadFromPreferences(tunnelManager) - try await saveToPreferences(tunnelManager) - } - private func saveToPreferences(_ tunnelManager: NETunnelProviderManager) async throws { do { try await tunnelManager.saveToPreferences() } catch { @@ -281,9 +289,7 @@ final class NetworkProtectionTunnelController: TunnelController { } throw StartError.saveToPreferencesFailed(error) } - } - private func loadFromPreferences(_ tunnelManager: NETunnelProviderManager) async throws { do { try await tunnelManager.loadFromPreferences() } catch { @@ -311,6 +317,31 @@ final class NetworkProtectionTunnelController: TunnelController { tunnelManager.onDemandRules = [NEOnDemandRuleConnect()] } + // MARK: - Observing Configuration Changes + + private func subscribeToConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + Task { @MainActor in + guard let manager = self.internalManager else { + return + } + + do { + try await manager.loadFromPreferences() + + if manager.connection.status == .invalid { + self.clearInternalManager() + } + } catch { + self.clearInternalManager() + } + } + } + .store(in: &cancellables) + } + // MARK: - Observing Status Changes private func subscribeToStatusChanges() { diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index 7a0a327087..214f675820 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -46,6 +46,7 @@ class MockDependencyProvider: DependencyProvider { var networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore var networkProtectionTunnelController: NetworkProtectionTunnelController var connectionObserver: NetworkProtection.ConnectionStatusObserver + var serverInfoObserver: NetworkProtection.ConnectionServerInfoObserver var vpnSettings: NetworkProtection.VPNSettings init() { @@ -88,6 +89,7 @@ class MockDependencyProvider: DependencyProvider { accountManager: accountManager) connectionObserver = ConnectionStatusObserverThroughSession() + serverInfoObserver = ConnectionServerInfoObserverThroughSession() vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) } }