From a4f4eb098fd2d072211b808a3aef3fea3099422f Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 6 Dec 2024 11:47:43 +0100 Subject: [PATCH 1/2] Alessandro/onboarding add to dock experiment setup (#3679) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208662187851716/f **Description**: This PR setup the Add to Dock experiment to be prompted from the onboarding flow. --- .../01_control_group_onboarding.yaml | 29 -- ...ing.yaml => 01_onboarding_contextual.yaml} | 3 +- .../02_control_group_hide_onboarding.yaml | 33 -- .../02_onboarding_add_to_dock_intro.yaml | 54 +++ .../03_onboarding_add_to_dock_contextual.yaml | 54 +++ .maestro/shared/onboarding.yaml | 23 +- Core/DefaultVariantManager.swift | 11 +- DuckDuckGo.xcodeproj/project.pbxproj | 8 - DuckDuckGo/DaxDialogs.swift | 167 +------ DuckDuckGo/DaxDialogsSettings.swift | 4 - .../Contents.json | 15 + .../OnboardingBackgroundLight.pdf | Bin 0 -> 64451 bytes DuckDuckGo/HomeRowReminder.swift | 13 +- .../MainViewController+AddFavoriteFlow.swift | 1 + DuckDuckGo/MainViewController+Segues.swift | 19 - DuckDuckGo/MainViewController.swift | 27 +- DuckDuckGo/NewTabPageViewController.swift | 42 +- DuckDuckGo/OnboardingDebugView.swift | 36 +- .../OnboardingView+AddToDockContent.swift | 15 - .../VideoPlayer/VideoPlayerView.swift | 8 +- ...ingAddressBarPositionPickerViewModel.swift | 10 +- .../AppIconPicker/AppIconPicker.swift | 2 - .../Background/OnboardingBackground.swift | 17 +- .../Background/OnboardingGradient.swift | 59 +-- .../BrowsersComparisonModel.swift | 21 +- .../ContextualOnboardingDialogs.swift | 30 +- .../NewTabDaxDialogFactory.swift | 31 +- .../ContextualDaxDialogsFactory.swift | 30 +- .../ContextualOnboardingPresenter.swift | 8 +- .../DefaultVariantManager+Onboarding.swift | 33 -- .../Manager/OnboardingManager.swift | 42 +- .../OnboardingIntroViewModel+Copy.swift | 20 +- .../OnboardingIntroViewModel.swift | 37 +- ...ardingView+AddressBarPositionContent.swift | 5 +- .../OnboardingView+AppIconPickerContent.swift | 7 +- ...ardingView+BrowsersComparisonContent.swift | 2 +- .../OnboardingView+IntroDialogContent.swift | 4 +- .../OnboardingIntro/OnboardingView.swift | 6 - .../Styles/DaxDialogStyles.swift | 30 +- .../OnboardingSuggestedSearchesProvider.swift | 49 +- DuckDuckGo/TabSwitcherViewController.swift | 14 +- DuckDuckGo/TabViewController.swift | 3 - DuckDuckGo/TabsBarViewController.swift | 22 +- DuckDuckGo/UserText.swift | 73 +-- DuckDuckGo/en.lproj/Localizable.strings | 36 -- .../BrowserComparisonModelTests.swift | 102 +--- .../ContextualOnboardingPresenterTests.swift | 74 +-- DuckDuckGoTests/DaxDialogTests.swift | 442 +++++------------- DuckDuckGoTests/DaxDialogsNewTabTests.swift | 1 - ...DefaultVariantManagerOnboardingTests.swift | 117 ----- DuckDuckGoTests/HomeRowReminderTests.swift | 36 ++ .../NewTabPageControllerDaxDialogTests.swift | 56 +-- ...dressBarPositionPickerViewModelTests.swift | 16 +- .../OnboardingDaxFavouritesTests.swift | 36 +- .../OnboardingIntroViewModelTests.swift | 148 +----- DuckDuckGoTests/OnboardingManagerMock.swift | 3 +- DuckDuckGoTests/OnboardingManagerTests.swift | 140 ++---- ...ardingSuggestedSearchesProviderTests.swift | 62 +-- .../TabViewControllerDaxDialogTests.swift | 6 - 59 files changed, 592 insertions(+), 1800 deletions(-) delete mode 100644 .maestro/onboarding_tests/01_control_group_onboarding.yaml rename .maestro/onboarding_tests/{03_experiment_group_linear_onboarding.yaml => 01_onboarding_contextual.yaml} (97%) delete mode 100644 .maestro/onboarding_tests/02_control_group_hide_onboarding.yaml create mode 100644 .maestro/onboarding_tests/02_onboarding_add_to_dock_intro.yaml create mode 100644 .maestro/onboarding_tests/03_onboarding_add_to_dock_contextual.yaml create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/OnboardingBackgroundLight.pdf delete mode 100644 DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift delete mode 100644 DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift diff --git a/.maestro/onboarding_tests/01_control_group_onboarding.yaml b/.maestro/onboarding_tests/01_control_group_onboarding.yaml deleted file mode 100644 index 609254b45a..0000000000 --- a/.maestro/onboarding_tests/01_control_group_onboarding.yaml +++ /dev/null @@ -1,29 +0,0 @@ -appId: com.duckduckgo.mobile.ios -tags: - - onboarding - ---- - -# Set up -- runFlow: - file: ../shared/setup.yaml - env: - ONBOARDING_COMPLETED: "false" - APP_VARIANT: "ma" - -# Load Site -- assertVisible: - id: "searchEntry" -- tapOn: - id: "searchEntry" -- inputText: "https://www.duckduckgo.com" -- pressKey: Enter - -# Handle Onboarding -- assertVisible: "Got It" -- assertVisible: "Hide" -- tapOn: "Got It" -- assertVisible: "Close Tabs and Clear Data" -- tapOn: "Close Tabs and Clear Data" -- tapOn: "Close Tabs and Clear Data" -- assertVisible: "You’ve got this!\n\nRemember: Every time you browse with me, a creepy ad loses its wings. 👍" diff --git a/.maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml b/.maestro/onboarding_tests/01_onboarding_contextual.yaml similarity index 97% rename from .maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml rename to .maestro/onboarding_tests/01_onboarding_contextual.yaml index 5a86cdfd17..d5fe31dc01 100644 --- a/.maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml +++ b/.maestro/onboarding_tests/01_onboarding_contextual.yaml @@ -9,7 +9,7 @@ tags: file: ../shared/setup.yaml env: ONBOARDING_COMPLETED: "false" - APP_VARIANT: "mb" + APP_VARIANT: "mh" # Handle Search Suggestions - assertVisible: "Ready to get started?\nTry a search!" @@ -50,3 +50,4 @@ tags: - assertVisible: "You’ve got this!" - assertVisible: "High five!" - tapOn: "High five!" + diff --git a/.maestro/onboarding_tests/02_control_group_hide_onboarding.yaml b/.maestro/onboarding_tests/02_control_group_hide_onboarding.yaml deleted file mode 100644 index a717b35c3a..0000000000 --- a/.maestro/onboarding_tests/02_control_group_hide_onboarding.yaml +++ /dev/null @@ -1,33 +0,0 @@ -appId: com.duckduckgo.mobile.ios -tags: - - onboarding - ---- - -# Set up -- runFlow: - file: ../shared/setup.yaml - env: - ONBOARDING_COMPLETED: "false" - APP_VARIANT: "ma" - -# Load Site -- assertVisible: - id: "searchEntry" -- tapOn: - id: "searchEntry" -- inputText: "https://www.duckduckgo.com" -- pressKey: Enter - -# Handle Onboarding -- assertVisible: "Got It" -- assertVisible: "Hide" -- tapOn: "Hide" -- assertVisible: "Hide Tips Forever" -- tapOn: "Hide Tips Forever" - -# Handle Fire Button -- assertVisible: "Close Tabs and Clear Data" -- tapOn: "Close Tabs and Clear Data" -- tapOn: "Close Tabs and Clear Data" -- assertNotVisible: "You’ve got this!\n\nRemember: Every time you browse with me, a creepy ad loses its wings. 👍" diff --git a/.maestro/onboarding_tests/02_onboarding_add_to_dock_intro.yaml b/.maestro/onboarding_tests/02_onboarding_add_to_dock_intro.yaml new file mode 100644 index 0000000000..8a354f5707 --- /dev/null +++ b/.maestro/onboarding_tests/02_onboarding_add_to_dock_intro.yaml @@ -0,0 +1,54 @@ +appId: com.duckduckgo.mobile.ios +tags: + - onboarding + +--- + +# Set up +- runFlow: + file: ../shared/setup.yaml + env: + ONBOARDING_COMPLETED: "false" + APP_VARIANT: "mk" + +# Handle Search Suggestions +- assertVisible: "Ready to get started?\nTry a search!" +- assertVisible: "Surprise Me!" +- tapOn: "Surprise Me!" + +# Handle First Dax Dialog +- assertVisible: "That’s DuckDuckGo Search. Private. Fast. Fewer ads." +- assertVisible: "Got It!" +- tapOn: "Got It!" + +# Handle Site Suggestions +- assertVisible: "Next, try visiting a site!" +- assertVisible: "Surprise Me!" +- tapOn: "Surprise Me!" + +# Handle Privacy Dashboard +- assertVisible: "Got It!" +- tapOn: + point: "6%,10%" # Shield icon. +- assertVisible: + text: "View Tracker Companies" +- assertVisible: + text: "Done" +- tapOn: "Done" + +# Handle Fire Message +- assertVisible: "Got It!" +- tapOn: "Got It!" +- assertVisible: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥" + +# Handle Fire Button +- assertVisible: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" + +# Handle End of Journey Dialog +- assertVisible: "You’ve got this!" +- assertVisible: "High five!" +- tapOn: "High five!" + + diff --git a/.maestro/onboarding_tests/03_onboarding_add_to_dock_contextual.yaml b/.maestro/onboarding_tests/03_onboarding_add_to_dock_contextual.yaml new file mode 100644 index 0000000000..967ad2d11d --- /dev/null +++ b/.maestro/onboarding_tests/03_onboarding_add_to_dock_contextual.yaml @@ -0,0 +1,54 @@ +appId: com.duckduckgo.mobile.ios +tags: + - onboarding + +--- + +# Set up +- runFlow: + file: ../shared/setup.yaml + env: + ONBOARDING_COMPLETED: "false" + APP_VARIANT: "mo" + +# Handle Search Suggestions +- assertVisible: "Ready to get started?\nTry a search!" +- assertVisible: "Surprise Me!" +- tapOn: "Surprise Me!" + +# Handle First Dax Dialog +- assertVisible: "That’s DuckDuckGo Search. Private. Fast. Fewer ads." +- assertVisible: "Got It!" +- tapOn: "Got It!" + +# Handle Site Suggestions +- assertVisible: "Next, try visiting a site!" +- assertVisible: "Surprise Me!" +- tapOn: "Surprise Me!" + +# Handle Privacy Dashboard +- assertVisible: "Got It!" +- tapOn: + point: "6%,10%" # Shield icon. +- assertVisible: + text: "View Tracker Companies" +- assertVisible: + text: "Done" +- tapOn: "Done" + +# Handle Fire Message +- assertVisible: "Got It!" +- tapOn: "Got It!" +- assertVisible: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥" + +# Handle Fire Button +- assertVisible: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" + +# Handle End of Journey Dialog +- assertVisible: "Add me to your Dock!" +- assertVisible: "Show Me How" +- tapOn: "Start Browsing" + + diff --git a/.maestro/shared/onboarding.yaml b/.maestro/shared/onboarding.yaml index 2117d4205b..154954b742 100644 --- a/.maestro/shared/onboarding.yaml +++ b/.maestro/shared/onboarding.yaml @@ -10,17 +10,24 @@ appId: com.duckduckgo.mobile.ios text: "Let’s Do It!" index: 0 -# Disabled while UI testing is happening -# - assertVisible: "Make DuckDuckGo your default browser." +# Browser comparison chart +# - assertVisible: "Protections activated!" - tapOn: text: "Skip" +# Add To Dock Flow - runFlow: when: - visible: "Which color looks best on me?" + visible: "Add me to your Dock!" commands: - - assertVisible: "Next" - - tapOn: "Next" - - assertVisible: "Where should I put your address bar?" - - assertVisible: "Next" - - tapOn: "Next" + - assertVisible: "Show Me How" + - tapOn: "Skip" + +# Customization Flow + +- assertVisible: "Which color looks best on me?" +- assertVisible: "Next" +- tapOn: "Next" +- assertVisible: "Where should I put your address bar?" +- assertVisible: "Next" +- tapOn: "Next" diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index 76f0f8475a..8e6748dd41 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -27,9 +27,8 @@ extension FeatureName { // Define your feature e.g.: // public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature") - public static let newOnboardingIntro = FeatureName(rawValue: "newOnboardingIntro") - public static let newOnboardingIntroHighlights = FeatureName(rawValue: "newOnboardingIntroHighlights") - public static let contextualDaxDialogs = FeatureName(rawValue: "contextualDaxDialogs") + public static let addToDockIntro = FeatureName(rawValue: "addToDockIntro") + public static let addToDockContextual = FeatureName(rawValue: "addToDockContextual") } public struct VariantIOS: Variant { @@ -58,9 +57,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: "ms", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]), - VariantIOS(name: "mu", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro, .contextualDaxDialogs]), - VariantIOS(name: "mx", weight: 1, isIncluded: When.always, features: [.newOnboardingIntroHighlights, .contextualDaxDialogs]), + VariantIOS(name: "mh", weight: 1, isIncluded: When.notPadDevice, features: []), + VariantIOS(name: "mk", weight: 1, isIncluded: When.notPadDevice, features: [.addToDockIntro]), + VariantIOS(name: "mo", weight: 1, isIncluded: When.notPadDevice, features: [.addToDockContextual]), returningUser ] diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c8cfaf060a..158ba01632 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -748,8 +748,6 @@ 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 */; }; - 9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */; }; - 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */; }; 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F1798572CD2443F0073018B /* AddToDockPromoViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; @@ -2593,8 +2591,6 @@ 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 = ""; }; - 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultVariantManager+Onboarding.swift"; sourceTree = ""; }; - 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVariantManagerOnboardingTests.swift; sourceTree = ""; }; 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToDockPromoViewModelTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; @@ -4969,7 +4965,6 @@ 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */, 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */, 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */, - 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */, 9F8E0F322CCA642D001EA7C5 /* VideoPlayerViewModelTests.swift */, 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */, ); @@ -5129,7 +5124,6 @@ 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, 9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */, 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */, - 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */, ); path = OnboardingExperiment; sourceTree = ""; @@ -7750,7 +7744,6 @@ 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, - 9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, @@ -8292,7 +8285,6 @@ 987130C7294AAB9F00AB05E0 /* MenuBookmarksViewModelTests.swift in Sources */, D6B9E8D42CDA8375002B640C /* DuckPlayerOverlayUsagePixelsTests.swift in Sources */, 858650D32469BFAD00C36F8A /* DaxDialogTests.swift in Sources */, - 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */, 31C138B227A4097800FFD4B2 /* DownloadTestsHelper.swift in Sources */, 1E1D8B5D2994FFE100C96994 /* AutoconsentMessageProtocolTests.swift in Sources */, 85C11E532090B23A00BFFEB4 /* UserDefaultsHomeRowReminderStorageTests.swift in Sources */, diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index d1d30b9e3a..f8b4181f93 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -31,7 +31,6 @@ protocol EntityProviding { } protocol NewTabDialogSpecProvider { - func nextHomeScreenMessage() -> DaxDialogs.HomeScreenSpec? func nextHomeScreenMessageNew() -> DaxDialogs.HomeScreenSpec? func dismiss() } @@ -50,7 +49,6 @@ protocol ContextualOnboardingLogic { func setPrivacyButtonPulseSeen() func setDaxDialogDismiss() - func canEnableAddFavoriteFlow() -> Bool // Temporary during Contextual Onboarding Experiment func enableAddFavoriteFlow() } @@ -144,14 +142,14 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { highlightAddressBar: false, pixelName: .daxDialogsSiteOwnedByMajorUnique, type: .siteOwnedByMajorTracker) - static let withOneTracker = BrowsingSpec(message: UserText.daxDialogBrowsingWithOneTracker, + static let withOneTracker = BrowsingSpec(message: UserText.Onboarding.ContextualOnboarding.daxDialogBrowsingWithOneTracker, cta: UserText.daxDialogBrowsingWithOneTrackerCTA, - highlightAddressBar: true, + highlightAddressBar: false, pixelName: .daxDialogsWithTrackersUnique, type: .withOneTracker) - static let withMultipleTrackers = BrowsingSpec(message: UserText.daxDialogBrowsingWithMultipleTrackers, + static let withMultipleTrackers = BrowsingSpec(message: UserText.Onboarding.ContextualOnboarding.daxDialogBrowsingWithMultipleTrackers, cta: UserText.daxDialogBrowsingWithMultipleTrackersCTA, - highlightAddressBar: true, + highlightAddressBar: false, pixelName: .daxDialogsWithTrackersUnique, type: .withMultipleTrackers) // Message and CTA empty on purpose as for this case we use only pixelName and type @@ -233,10 +231,6 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { self.addToDockManager = onboardingManager } - private var isNewOnboarding: Bool { - variantManager.isContextualDaxDialogsEnabled - } - private var firstBrowsingMessageSeen: Bool { return settings.browsingAfterSearchShown || settings.browsingWithTrackersShown @@ -272,17 +266,14 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } var isShowingSearchSuggestions: Bool { - guard isNewOnboarding else { return false } return currentHomeSpec == .initial } var isShowingSitesSuggestions: Bool { - guard isNewOnboarding else { return false } return lastShownDaxDialogType.flatMap(BrowsingSpec.SpecType.init(rawValue:)) == .visitWebsite || currentHomeSpec == .subsequent } var isShowingAddToDockDialog: Bool { - guard isNewOnboarding else { return false } return currentHomeSpec == .final && addToDockManager.addToDockEnabledState == .contextual } @@ -293,7 +284,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } var isShowingFireDialog: Bool { - guard isNewOnboarding, let lastShownDaxDialogType else { return false } + guard let lastShownDaxDialogType else { return false } return BrowsingSpec.SpecType(rawValue: lastShownDaxDialogType) == .fire } @@ -302,25 +293,16 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } var shouldShowFireButtonPulse: Bool { - if isNewOnboarding { - // Show fire the user hasn't seen the fire education dialog or the fire button has not animated before. - nonDDGBrowsingMessageSeen && (!settings.fireMessageExperimentShown && settings.fireButtonPulseDateShown == nil) && isEnabled - } else { - nonDDGBrowsingMessageSeen && !settings.fireButtonEducationShownOrExpired && isEnabled - } + // Show fire the user hasn't seen the fire education dialog or the fire button has not animated before. + nonDDGBrowsingMessageSeen && (!settings.fireMessageExperimentShown && settings.fireButtonPulseDateShown == nil) && isEnabled } var shouldShowPrivacyButtonPulse: Bool { - guard isNewOnboarding else { return false } return settings.browsingWithTrackersShown && !settings.privacyButtonPulseShown && fireButtonPulseTimer == nil && isEnabled } func isStillOnboarding() -> Bool { - if isNewOnboarding { - if peekNextHomeScreenMessageExperiment() != nil { - return true - } - } else if peekNextHomeScreenMessage() != nil { + if peekNextHomeScreenMessageExperiment() != nil { return true } return false @@ -336,16 +318,8 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { settings.isDismissed = false } - func canEnableAddFavoriteFlow() -> Bool { - !isNewOnboarding - } - func enableAddFavoriteFlow() { - guard canEnableAddFavoriteFlow() else { return } - nextHomeScreenMessageOverride = .addFavorite - // Progress to next home screen message, but don't re-show the second dax dialog if it's already been shown - settings.homeScreenMessagesSeen = max(settings.homeScreenMessagesSeen, 1) } func resumeRegularFlow() { @@ -360,22 +334,19 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { private static let timeToFireButtonExpire: TimeInterval = 1 * 60 * 60 private var lastVisitedOnboardingWebsiteURLPath: String? { - guard isNewOnboarding else { return nil } return settings.lastVisitedOnboardingWebsiteURLPath } private func saveLastVisitedOnboardingWebsite(url: URL?) { - guard isNewOnboarding, let url = url else { return } + guard let url = url else { return } settings.lastVisitedOnboardingWebsiteURLPath = url.absoluteString } private func removeLastVisitedOnboardingWebsite() { - guard isNewOnboarding else { return } settings.lastVisitedOnboardingWebsiteURLPath = nil } private var lastShownDaxDialogType: String? { - guard isNewOnboarding else { return nil } return settings.lastShownContextualOnboardingDialogType } @@ -384,12 +355,10 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } private func saveLastShownDaxDialog(specType: BrowsingSpec.SpecType) { - guard isNewOnboarding else { return } settings.lastShownContextualOnboardingDialogType = specType.rawValue } private func removeLastShownDaxDialog() { - guard isNewOnboarding else { return } settings.lastShownContextualOnboardingDialogType = nil } @@ -404,13 +373,13 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return BrowsingSpec.withoutTrackers case BrowsingSpec.SpecType.siteIsMajorTracker.rawValue: guard let host = privacyInfo.domain else { return nil } - return majorTrackerMessage(host) + return majorTrackerMessage(host, isReloadingDialog: true) case BrowsingSpec.SpecType.siteOwnedByMajorTracker.rawValue: guard let host = privacyInfo.domain, let owner = isOwnedByFacebookOrGoogle(host) else { return nil } - return majorTrackerOwnerMessage(host, owner) + return majorTrackerOwnerMessage(host, owner, isReloadingDialog: true) case BrowsingSpec.SpecType.withOneTracker.rawValue, BrowsingSpec.SpecType.withMultipleTrackers.rawValue: guard let entityNames = blockedEntityNames(privacyInfo.trackerInfo) else { return nil } - return trackersBlockedMessage(entityNames) + return trackersBlockedMessage(entityNames, isReloadingDialog: true) case BrowsingSpec.SpecType.fire.rawValue: return .fire case BrowsingSpec.SpecType.final.rawValue: @@ -439,20 +408,12 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { fireButtonPulseTimer?.invalidate() settings.fireButtonEducationShownOrExpired = true } - - func fireButtonEducationMessage() -> ActionSheetSpec? { - guard shouldShowFireButtonPulse else { return nil } - settings.fireButtonEducationShownOrExpired = true - return ActionSheetSpec.fireButtonEducation - } func setSearchMessageSeen() { - guard isNewOnboarding else { return } saveLastShownDaxDialog(specType: .visitWebsite) } func setFireEducationMessageSeen() { - guard isNewOnboarding else { return } // Set also privacy button pulse seen as we don't have to show anymore if we saw the fire educational message. settings.privacyButtonPulseShown = true settings.fireMessageExperimentShown = true @@ -460,35 +421,24 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } func clearedBrowserData() { - guard isNewOnboarding else { return } setDaxDialogDismiss() } func setPrivacyButtonPulseSeen() { - guard isNewOnboarding else { return } settings.privacyButtonPulseShown = true } func setDaxDialogDismiss() { - guard isNewOnboarding else { return } clearOnboardingBrowsingData() } func setFinalOnboardingDialogSeen() { - guard isNewOnboarding else { return } settings.browsingFinalDialogShown = true } func nextBrowsingMessageIfShouldShow(for privacyInfo: PrivacyInfo) -> BrowsingSpec? { - var message: BrowsingSpec? - if isNewOnboarding { - message = nextBrowsingMessageExperiment(privacyInfo: privacyInfo) - } else { - guard privacyInfo.url != lastURLDaxDialogReturnedFor else { return nil } - message = nextBrowsingMessage(privacyInfo: privacyInfo) - } - + let message = nextBrowsingMessageExperiment(privacyInfo: privacyInfo) if message != nil { lastURLDaxDialogReturnedFor = privacyInfo.url } @@ -496,32 +446,6 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return message } - private func nextBrowsingMessage(privacyInfo: PrivacyInfo) -> BrowsingSpec? { - guard isEnabled, nextHomeScreenMessageOverride == nil else { return nil } - guard let host = privacyInfo.domain else { return nil } - - if privacyInfo.url.isDuckDuckGoSearch { - return searchMessage() - } - - // won't be shown if owned by major tracker message has already been shown - if isFacebookOrGoogle(privacyInfo.url) { - return majorTrackerMessage(host) - } - - // won't be shown if major tracker message has already been shown - if let owner = isOwnedByFacebookOrGoogle(host) { - return majorTrackerOwnerMessage(host, owner) - } - - if let entityNames = blockedEntityNames(privacyInfo.trackerInfo) { - return trackersBlockedMessage(entityNames) - } - - // only shown if first time on a non-ddg page and none of the non-ddg messages shown - return noTrackersMessage() - } - private func nextBrowsingMessageExperiment(privacyInfo: PrivacyInfo) -> BrowsingSpec? { func hasTrackers(host: String) -> Bool { @@ -546,12 +470,12 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { spec = searchMessage() } else if isFacebookOrGoogle(privacyInfo.url) && shouldShowNetworkTrackerDialog { // won't be shown if owned by major tracker message has already been shown - spec = majorTrackerMessage(host) + spec = majorTrackerMessage(host, isReloadingDialog: false) } else if let owner = isOwnedByFacebookOrGoogle(host), shouldShowNetworkTrackerDialog { // won't be shown if major tracker message has already been shown - spec = majorTrackerOwnerMessage(host, owner) + spec = majorTrackerOwnerMessage(host, owner, isReloadingDialog: false) } else if let entityNames = blockedEntityNames(privacyInfo.trackerInfo), !settings.browsingWithTrackersShown { - spec = trackersBlockedMessage(entityNames) + spec = trackersBlockedMessage(entityNames, isReloadingDialog: false) } else if !settings.browsingWithoutTrackersShown && !privacyInfo.url.isDuckDuckGoSearch && !hasTrackers(host: host) { // if non duck duck go search and no trackers found and no tracker message already shown, show no trackers message spec = noTrackersMessage() @@ -570,16 +494,6 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return spec } - func nextHomeScreenMessage() -> HomeScreenSpec? { - guard let homeScreenSpec = peekNextHomeScreenMessage() else { return nil } - - if homeScreenSpec != nextHomeScreenMessageOverride { - settings.homeScreenMessagesSeen += 1 - } - - return homeScreenSpec - } - func nextHomeScreenMessageNew() -> HomeScreenSpec? { // Reset the last browsing information when opening a new tab so loading the previous website won't show again the Dax dialog clearedBrowserData() @@ -592,25 +506,6 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return homeScreenSpec } - private func peekNextHomeScreenMessage() -> HomeScreenSpec? { - if nextHomeScreenMessageOverride != nil { - return nextHomeScreenMessageOverride - } - - guard isEnabled else { return nil } - guard settings.homeScreenMessagesSeen < Constants.homeScreenMessagesSeenMaxCeiling else { return nil } - - if settings.homeScreenMessagesSeen == 0 { - return .initial - } - - if firstBrowsingMessageSeen { - return .final - } - - return nil - } - private func peekNextHomeScreenMessageExperiment() -> HomeScreenSpec? { if nextHomeScreenMessageOverride != nil { return nextHomeScreenMessageOverride @@ -645,9 +540,9 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return nil } - func majorTrackerOwnerMessage(_ host: String, _ majorTrackerEntity: Entity) -> DaxDialogs.BrowsingSpec? { - if !isNewOnboarding && settings.browsingMajorTrackingSiteShown { return nil } - + func majorTrackerOwnerMessage(_ host: String, _ majorTrackerEntity: Entity, isReloadingDialog: Bool) -> DaxDialogs.BrowsingSpec? { + if !isReloadingDialog && settings.browsingMajorTrackingSiteShown { return nil } + guard let entityName = majorTrackerEntity.displayName, let entityPrevalence = majorTrackerEntity.prevalence else { return nil } settings.browsingMajorTrackingSiteShown = true @@ -657,8 +552,8 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { entityPrevalence) } - private func majorTrackerMessage(_ host: String) -> DaxDialogs.BrowsingSpec? { - if !isNewOnboarding && settings.browsingMajorTrackingSiteShown { return nil } + private func majorTrackerMessage(_ host: String, isReloadingDialog: Bool) -> DaxDialogs.BrowsingSpec? { + if !isReloadingDialog && settings.browsingMajorTrackingSiteShown { return nil } guard let entityName = entityProviding.entity(forHost: host)?.displayName else { return nil } settings.browsingMajorTrackingSiteShown = true @@ -677,8 +572,8 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return BrowsingSpec.final } - private func trackersBlockedMessage(_ entitiesBlocked: [String]) -> BrowsingSpec? { - if !isNewOnboarding && settings.browsingWithTrackersShown { return nil } + private func trackersBlockedMessage(_ entitiesBlocked: [String], isReloadingDialog: Bool) -> BrowsingSpec? { + if !isReloadingDialog && settings.browsingWithTrackersShown { return nil } var spec: BrowsingSpec? switch entitiesBlocked.count { @@ -689,24 +584,12 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { case 1: settings.browsingWithTrackersShown = true let args = entitiesBlocked[0] - spec = if isNewOnboarding { - BrowsingSpec.withOneTracker.format(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.daxDialogBrowsingWithOneTracker, args: args) - } else { - BrowsingSpec.withOneTracker.format(args: args) - } + spec = BrowsingSpec.withOneTracker.format(message: UserText.Onboarding.ContextualOnboarding.daxDialogBrowsingWithOneTracker, args: args) default: settings.browsingWithTrackersShown = true let args: [CVarArg] = [entitiesBlocked.count - 2, entitiesBlocked[0], entitiesBlocked[1]] - spec = if isNewOnboarding { - BrowsingSpec.withMultipleTrackers.format(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.daxDialogBrowsingWithMultipleTrackers, args: args) - } else { - BrowsingSpec.withMultipleTrackers.format(args: args) - } - } - // New Contextual onboarding doesn't highlight the address bar. This checks prevents to cancel the lottie animation. - if isNewOnboarding { - spec?.highlightAddressBar = false + spec = BrowsingSpec.withMultipleTrackers.format(message: UserText.Onboarding.ContextualOnboarding.daxDialogBrowsingWithMultipleTrackers, args: args) } return spec } diff --git a/DuckDuckGo/DaxDialogsSettings.swift b/DuckDuckGo/DaxDialogsSettings.swift index 960b63013a..63be0c9973 100644 --- a/DuckDuckGo/DaxDialogsSettings.swift +++ b/DuckDuckGo/DaxDialogsSettings.swift @@ -23,8 +23,6 @@ protocol DaxDialogsSettings { var isDismissed: Bool { get set } - var homeScreenMessagesSeen: Int { get set } - var browsingAfterSearchShown: Bool { get set } var browsingWithTrackersShown: Bool { get set } @@ -96,8 +94,6 @@ class InMemoryDaxDialogsSettings: DaxDialogsSettings { var isDismissed: Bool = false - var homeScreenMessagesSeen: Int = 0 - var browsingAfterSearchShown: Bool = false var browsingWithTrackersShown: Bool = false diff --git a/DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/Contents.json new file mode 100644 index 0000000000..5a553e162d --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "OnboardingBackgroundLight.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/OnboardingBackgroundLight.pdf b/DuckDuckGo/DaxOnboarding.xcassets/OnboardingGradientLight.imageset/OnboardingBackgroundLight.pdf new file mode 100644 index 0000000000000000000000000000000000000000..24684a0c471d5df8957c228b8d0f3bd98532e9c1 GIT binary patch literal 64451 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/HomeRowReminder.swift b/DuckDuckGo/HomeRowReminder.swift index 7d0ced914e..7c8a1c95ed 100644 --- a/DuckDuckGo/HomeRowReminder.swift +++ b/DuckDuckGo/HomeRowReminder.swift @@ -18,6 +18,7 @@ // import Core +import BrowserServicesKit protocol HomeRowReminderStorage { @@ -36,14 +37,22 @@ class HomeRowReminder { private var storage: HomeRowReminderStorage private var homeMessageStorage: HomeMessageStorage + private let variantManager: VariantManager - init(storage: HomeRowReminderStorage = UserDefaultsHomeRowReminderStorage(), - homeMessageStorage: HomeMessageStorage = HomeMessageStorage()) { + init( + storage: HomeRowReminderStorage = UserDefaultsHomeRowReminderStorage(), + homeMessageStorage: HomeMessageStorage = HomeMessageStorage(), + variantManager: VariantManager = DefaultVariantManager() + ) { self.storage = storage self.homeMessageStorage = homeMessageStorage + self.variantManager = variantManager } func showNow() -> Bool { + // If user saw Add to Dock instruction during onboarding do not show the reminder. + guard !(variantManager.isSupported(feature: .addToDockIntro) || variantManager.isSupported(feature: .addToDockContextual)) else { return false } + guard !hasShownBefore() else { return false } guard hasReminderTimeElapsed else { return false } return true diff --git a/DuckDuckGo/MainViewController+AddFavoriteFlow.swift b/DuckDuckGo/MainViewController+AddFavoriteFlow.swift index 78e47a03c6..8231cc52bc 100644 --- a/DuckDuckGo/MainViewController+AddFavoriteFlow.swift +++ b/DuckDuckGo/MainViewController+AddFavoriteFlow.swift @@ -48,6 +48,7 @@ extension MainViewController { guard canDisplayAddFavoriteVisualIndicator, let window = view.window, presentedViewController == nil else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + guard self.canDisplayAddFavoriteVisualIndicator else { return } ViewHighlighter.hideAll() ViewHighlighter.showIn(window, focussedOnView: self.presentedMenuButton) } diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index beee686007..2ca6573a8a 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -98,25 +98,6 @@ extension MainViewController { } } - func segueToActionSheetDaxDialogWithSpec(_ spec: DaxDialogs.ActionSheetSpec) { - Logger.lifecycle.debug(#function) - hideAllHighlightsIfNeeded() - - if spec == DaxDialogs.ActionSheetSpec.fireButtonEducation { - ViewHighlighter.hideAll() - } - - let storyboard = UIStoryboard(name: "DaxOnboarding", bundle: nil) - let controller = storyboard.instantiateViewController(identifier: "ActionSheetDaxDialog", creator: { coder in - ActionSheetDaxDialogViewController(coder: coder) - }) - controller.spec = spec - controller.delegate = self - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .overFullScreen - present(controller, animated: true) - } - func segueToReportBrokenSite(entryPoint: PrivacyDashboardEntryPoint = .report) { Logger.lifecycle.debug(#function) hideAllHighlightsIfNeeded() diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index e5b54dca00..2ee59fc07f 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -477,12 +477,6 @@ class MainViewController: UIViewController { } func startAddFavoriteFlow() { - // Disable add favourite flow when new onboarding experiment is running and open a new tab. - guard contextualOnboardingLogic.canEnableAddFavoriteFlow() else { - newTab() - return - } - contextualOnboardingLogic.enableAddFavoriteFlow() if tutorialSettings.hasSeenOnboarding { newTab() @@ -877,18 +871,10 @@ class MainViewController: UIViewController { hideNotificationBarIfBrokenSitePromptShown() wakeLazyFireButtonAnimator() - if variantManager.isContextualDaxDialogsEnabled { - // Dismiss dax dialog and pulse animation when the user taps on the Fire Button. - currentTab?.dismissContextualDaxFireDialog() - ViewHighlighter.hideAll() - showClearDataAlert() - } else { - if let spec = DaxDialogs.shared.fireButtonEducationMessage() { - segueToActionSheetDaxDialogWithSpec(spec) - } else { - showClearDataAlert() - } - } + // Dismiss dax dialog and pulse animation when the user taps on the Fire Button. + currentTab?.dismissContextualDaxFireDialog() + ViewHighlighter.hideAll() + showClearDataAlert() performCancel() } @@ -2757,9 +2743,8 @@ extension MainViewController: AutoClearWorker { self.showKeyboardAfterFireButton = showKeyboardAfterFireButton } - if self.variantManager.isContextualDaxDialogsEnabled { - DaxDialogs.shared.clearedBrowserData() - } + DaxDialogs.shared.clearedBrowserData() + } } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index 02c0009e75..a9ebe988a0 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -210,11 +210,6 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { self.launchNewSearch() } } - - if !variantManager.isContextualDaxDialogsEnabled { - // In the new onboarding this gets called twice (viewDidAppear in Tab) which then reset the spec to nil. - presentNextDaxDialog() - } } func dismiss() { @@ -239,11 +234,7 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { // MARK: - Onboarding private func presentNextDaxDialog() { - if variantManager.isContextualDaxDialogsEnabled { - showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) - } else { - showNextDaxDialog(dialogProvider: newTabDialogTypeProvider) - } + showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) } // MARK: - Private @@ -280,37 +271,6 @@ extension NewTabPageViewController: HomeScreenTransitionSource { extension NewTabPageViewController { - func showNextDaxDialog(dialogProvider: NewTabDialogSpecProvider) { - guard let spec = dialogProvider.nextHomeScreenMessage() else { return } - guard !isDaxDialogVisible else { return } - guard let daxDialogViewController = daxDialogViewController else { return } - - newTabPageViewModel.startOnboarding() - - daxDialogViewController.view.isHidden = false - daxDialogViewController.view.alpha = 0.0 - - daxDialogViewController.loadViewIfNeeded() - daxDialogViewController.message = spec.message - daxDialogViewController.accessibleMessage = spec.accessibilityLabel - - if spec == .initial { - UniquePixel.fire(pixel: .onboardingContextualTryVisitSiteUnique, includedParameters: [.appVersion, .atb]) - } - - view.addGestureRecognizer(daxDialogViewController.tapToCompleteGestureRecognizer) - - daxDialogHeightConstraint?.constant = daxDialogViewController.calculateHeight() - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - UIView.animate(withDuration: 0.4, animations: { - daxDialogViewController.view.alpha = 1.0 - }, completion: { _ in - daxDialogViewController.start() - }) - } - } - func showNextDaxDialogNew(dialogProvider: NewTabDialogSpecProvider, factory: any NewTabDaxDialogProvider) { dismissHostingController(didFinishNTPOnboarding: false) diff --git a/DuckDuckGo/OnboardingDebugView.swift b/DuckDuckGo/OnboardingDebugView.swift index f6528aea25..93c4ad3ba2 100644 --- a/DuckDuckGo/OnboardingDebugView.swift +++ b/DuckDuckGo/OnboardingDebugView.swift @@ -32,19 +32,6 @@ struct OnboardingDebugView: View { var body: some View { List { - Section { - Toggle( - isOn: $viewModel.isOnboardingHighlightsLocalFlagEnabled, - label: { - Text(verbatim: "Onboarding Highlights local setting enabled") - } - ) - } header: { - Text(verbatim: "Onboarding Higlights settings") - } footer: { - Text(verbatim: "Requires internal user flag set to have an effect.") - } - Section { Picker( selection: $viewModel.onboardingAddToDockLocalFlagState, @@ -57,10 +44,11 @@ struct OnboardingDebugView: View { Text(verbatim: "Onboarding Add to Dock local setting enabled") } ) + .disabled(!viewModel.isIphone) } header: { Text(verbatim: "Onboarding Add to Dock settings") } footer: { - Text(verbatim: "Requires internal user flag set to have an effect.") + Text(verbatim: viewModel.isIphone ? "Requires internal user flag set to have an effect." : "Requires internal user flag set to have an effect. iPhone only feature.") } Section { @@ -77,8 +65,7 @@ struct OnboardingDebugView: View { Section { Button(action: newOnboardingIntroStartAction, label: { - let onboardingType = viewModel.isOnboardingHighlightsLocalFlagEnabled ? "Highlights" : "" - Text(verbatim: "Preview New Onboarding Intro \(onboardingType)") + Text(verbatim: "Preview Onboarding Intro") }) } } @@ -86,11 +73,6 @@ struct OnboardingDebugView: View { } final class OnboardingDebugViewModel: ObservableObject { - @Published var isOnboardingHighlightsLocalFlagEnabled: Bool { - didSet { - manager.isOnboardingHighlightsLocalFlagEnabled = isOnboardingHighlightsLocalFlagEnabled - } - } @Published var onboardingAddToDockLocalFlagState: OnboardingAddToDockState { didSet { @@ -98,19 +80,23 @@ final class OnboardingDebugViewModel: ObservableObject { } } - private let manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging + private let manager: OnboardingAddToDockDebugging private var settings: DaxDialogsSettings + let isIphone: Bool - init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager(), settings: DaxDialogsSettings = DefaultDaxDialogsSettings()) { + init( + manager: OnboardingAddToDockDebugging = OnboardingManager(), + settings: DaxDialogsSettings = DefaultDaxDialogsSettings(), + isIphone: Bool = UIDevice.current.userInterfaceIdiom == .phone + ) { self.manager = manager self.settings = settings - isOnboardingHighlightsLocalFlagEnabled = manager.isOnboardingHighlightsLocalFlagEnabled + self.isIphone = isIphone onboardingAddToDockLocalFlagState = manager.addToDockLocalFlagState } func resetDaxDialogs() { settings.isDismissed = false - settings.homeScreenMessagesSeen = 0 settings.browsingAfterSearchShown = false settings.browsingWithTrackersShown = false settings.browsingWithoutTrackersShown = false diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift index c1311dec7f..af74e72b42 100644 --- a/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift @@ -22,32 +22,17 @@ import Onboarding extension OnboardingView { - struct AddToDockPromoContentState { - var animateTitle = true - var animateMessage = false - var showContent = false - } - struct AddToDockPromoContent: View { @State private var showAddToDockTutorial = false - private var animateTitle: Binding - private var animateMessage: Binding - private var showContent: Binding private let showTutorialAction: () -> Void private let dismissAction: (_ fromAddToDock: Bool) -> Void init( - animateTitle: Binding = .constant(true), - animateMessage: Binding = .constant(true), - showContent: Binding = .constant(false), showTutorialAction: @escaping () -> Void, dismissAction: @escaping (_ fromAddToDock: Bool) -> Void ) { - self.animateTitle = animateTitle - self.animateMessage = animateMessage - self.showContent = showContent self.showTutorialAction = showTutorialAction self.dismissAction = dismissAction } diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/VideoPlayer/VideoPlayerView.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/VideoPlayer/VideoPlayerView.swift index c840bb7555..ea8fccf72a 100644 --- a/DuckDuckGo/OnboardingExperiment/AddToDock/VideoPlayer/VideoPlayerView.swift +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/VideoPlayer/VideoPlayerView.swift @@ -24,7 +24,7 @@ struct VideoPlayerView: View { @ObservedObject private var model: VideoPlayerViewModel - private var isPlaying: Binding + private var isPlaying: Binding init(model: VideoPlayerViewModel, isPlaying: Binding = .constant(true)) { self.model = model @@ -41,6 +41,12 @@ struct VideoPlayerView: View { model.pause() } } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification), perform: { _ in + isPlaying.wrappedValue = false + }) + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + isPlaying.wrappedValue = true + } } } diff --git a/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift index 595fb5c061..5bfe88fcae 100644 --- a/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift @@ -74,22 +74,22 @@ private extension AddressBarPosition { var titleAndMessage: (title: NSAttributedString, message: String) { switch self { case .top: - let firstPart = NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle) + let firstPart = NSAttributedString(string: UserText.Onboarding.AddressBarPosition.topTitle) .withFont(UIFont.daxBodyBold()) .withTextColor(UIColor.label) - let secondPart = NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) + let secondPart = NSAttributedString(string: UserText.Onboarding.AddressBarPosition.defaultOption) .withFont(UIFont.daxBodyRegular()) .withTextColor(UIColor.secondaryLabel) return ( firstPart + " " + secondPart, - UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage + UserText.Onboarding.AddressBarPosition.topMessage ) case .bottom: return ( - NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) + NSAttributedString(string: UserText.Onboarding.AddressBarPosition.bottomTitle) .withFont(UIFont.daxBodyBold()), - UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage + UserText.Onboarding.AddressBarPosition.bottomMessage ) } } diff --git a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift index 2d1dd5ea4d..6fbb17708a 100644 --- a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift +++ b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift @@ -30,8 +30,6 @@ private enum Metrics { } struct AppIconPicker: View { - @Environment(\.colorScheme) private var color - @StateObject private var viewModel = AppIconPickerViewModel() let layout = [GridItem(.adaptive(minimum: Metrics.iconSize), spacing: Metrics.spacing, alignment: .leading)] diff --git a/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift b/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift index 7e5b6fb531..b1dcdc5ddd 100644 --- a/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift +++ b/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift @@ -20,7 +20,6 @@ import SwiftUI struct OnboardingBackground: View { - @Environment(\.onboardingGradientType) private var gradientType @Environment(\.verticalSizeClass) private var vSizeClass @Environment(\.horizontalSizeClass) private var hSizeClass @Environment(\.colorScheme) private var colorScheme @@ -35,7 +34,7 @@ struct OnboardingBackground: View { .opacity(colorScheme == .light ? 0.5 : 0.3) .frame(width: proxy.size.width, height: proxy.size.height, alignment: alignment) .background( - OnboardingGradientView(type: gradientType) + OnboardingGradientView() .ignoresSafeArea() ) } @@ -48,24 +47,10 @@ private enum Metrics { #Preview("Light Mode") { OnboardingBackground() - .onboardingGradient(.default) - .preferredColorScheme(.light) -} - -#Preview("Dark Mode") { - OnboardingBackground() - .onboardingGradient(.default) - .preferredColorScheme(.dark) -} - -#Preview("Light Mode - Highlights") { - OnboardingBackground() - .onboardingGradient(.highlights) .preferredColorScheme(.light) } #Preview("Dark Mode - Highlights") { OnboardingBackground() - .onboardingGradient(.highlights) .preferredColorScheme(.dark) } diff --git a/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift b/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift index c5b6fff5a6..d32dfa3202 100644 --- a/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift +++ b/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift @@ -23,57 +23,22 @@ import Onboarding struct OnboardingGradientView: View { @Environment(\.colorScheme) private var colorScheme - private let type: OnboardingGradientType - - init(type: OnboardingGradientType) { - self.type = type - } - var body: some View { - switch (type, colorScheme) { - case (.default, .light): - linearLightGradient - case (.default, .dark): - linearDarkGradient - case (.highlights, _): - // If highlights experiment use new common gradient for iOS and macOS + switch colorScheme { + case .dark: OnboardingGradient() + case .light: + // iOS 15 doesn't render properly the light EllipticalGradient while the Dark gradient is rendered correctly + // https://app.asana.com/0/1206329551987282/1208839072951158/f + if #available(iOS 16, *) { + OnboardingGradient() + } else { + Image(.onboardingGradientLight) + .resizable() + } @unknown default: - linearLightGradient + OnboardingGradient() } } - private var linearLightGradient: 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 linearDarkGradient: 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) - ) - } - -} - -enum OnboardingGradientType { - case `default` - case highlights } diff --git a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift index 6f4f855868..c7011b0b1f 100644 --- a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift +++ b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift @@ -117,29 +117,18 @@ extension BrowsersComparisonModel.PrivacyFeature { case blockCreepyAds case eraseBrowsingData - // Remove it once Highlights experiment finishes - static var onboardingManager: OnboardingHighlightsManaging = OnboardingManager() - var title: String { switch self { case .privateSearch: - UserText.DaxOnboardingExperiment.BrowsersComparison.Features.privateSearch + UserText.Onboarding.BrowsersComparison.Features.privateSearch case .blockThirdPartyTrackers: - Self.onboardingManager.isOnboardingHighlightsEnabled ? - UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers : - UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers + UserText.Onboarding.BrowsersComparison.Features.trackerBlockers case .blockCookiePopups: - Self.onboardingManager.isOnboardingHighlightsEnabled ? - UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups: - UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups + UserText.Onboarding.BrowsersComparison.Features.cookiePopups case .blockCreepyAds: - Self.onboardingManager.isOnboardingHighlightsEnabled ? - UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds : - UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds + UserText.Onboarding.BrowsersComparison.Features.creepyAds case .eraseBrowsingData: - Self.onboardingManager.isOnboardingHighlightsEnabled ? - UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData: - UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData + UserText.Onboarding.BrowsersComparison.Features.eraseBrowsingData } } } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index b3e61bea54..abdbbec806 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -23,7 +23,7 @@ import Onboarding import DuckUI struct OnboardingTrySearchDialog: View { - let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchTitle + let title = UserText.Onboarding.ContextualOnboarding.onboardingTryASearchTitle let message: String let viewModel: OnboardingSearchSuggestionsViewModel @@ -58,7 +58,7 @@ struct OnboardingTryVisitingSiteDialog: View { } struct OnboardingTryVisitingSiteDialogContent: View { - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteMessage) + let message = NSAttributedString(string: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteMessage) let viewModel: OnboardingSiteSuggestionsViewModel @@ -74,7 +74,7 @@ struct OnboardingTryVisitingSiteDialogContent: View { struct OnboardingFireButtonDialogContent: View { private let attributedMessage: NSAttributedString = { - let firstString = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryFireButtonMessage + let firstString = UserText.Onboarding.ContextualOnboarding.onboardingTryFireButtonMessage let boldString = "Fire Button." let attributedString = NSMutableAttributedString(string: firstString) let boldFontAttribute: [NSAttributedString.Key: Any] = [ @@ -95,7 +95,7 @@ struct OnboardingFireButtonDialogContent: View { } struct OnboardingFirstSearchDoneDialog: View { - let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingGotItButton + let cta = UserText.Onboarding.ContextualOnboarding.onboardingGotItButton let message: NSAttributedString @State private var showNextScreen: Bool = false @@ -147,7 +147,7 @@ struct OnboardingFireDialog: View { } struct OnboardingTrackersDoneDialog: View { - let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingGotItButton + let cta = UserText.Onboarding.ContextualOnboarding.onboardingGotItButton @State private var showNextScreen: Bool = false @@ -202,7 +202,7 @@ struct OnboardingFinalDialog: View { } } else { ContextualDaxDialogContent( - title: canShowAddToDockTutorial ? UserText.AddToDockOnboarding.Promo.title : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenTitle, + title: canShowAddToDockTutorial ? UserText.AddToDockOnboarding.Promo.title : UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenTitle, titleFont: Font(UIFont.daxTitle3()), message: NSAttributedString(string: message), messageFont: Font.system(size: 16), @@ -295,17 +295,17 @@ struct OnboardingAddToDockTutorialContent: View { // MARK: - Preview #Preview("Try Search") { - OnboardingTrySearchDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage, viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) + OnboardingTrySearchDialog(message: UserText.Onboarding.ContextualOnboarding.onboardingTryASearchMessage, viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) .padding() } #Preview("Try Site Top") { - OnboardingTryVisitingSiteDialog(logoPosition: .top, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter())) + OnboardingTryVisitingSiteDialog(logoPosition: .top, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter())) .padding() } #Preview("Try Site Left") { - OnboardingTryVisitingSiteDialog(logoPosition: .left, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter())) + OnboardingTryVisitingSiteDialog(logoPosition: .left, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter())) .padding() } @@ -317,15 +317,21 @@ struct OnboardingAddToDockTutorialContent: View { } #Preview("First Search Dialog") { - OnboardingFirstSearchDoneDialog(message: NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage), shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) + let attributedMessage = { + let message = UserText.Onboarding.ContextualOnboarding.onboardingFirstSearchDoneMessage + let boldRange = message.range(of: "DuckDuckGo Search") + return message.attributed.with(attribute: .font, value: UIFont.daxBodyBold(), in: boldRange) + }() + + return OnboardingFirstSearchDoneDialog(message: attributedMessage, shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) .padding() } #Preview("Final Dialog - No Add to Dock Tutorial") { OnboardingFinalDialog( logoPosition: .top, - message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, - cta: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton, + message: UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenMessage, + cta: UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenButton, canShowAddToDockTutorial: false, showAddToDockTutorialAction: {}, dismissAction: { _ in } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index 1dce0528fd..994078c2d7 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -30,17 +30,13 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { private var delegate: OnboardingNavigationDelegate? private let contextualOnboardingLogic: ContextualOnboardingLogic private let onboardingPixelReporter: OnboardingPixelReporting - private let onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging - - private var gradientType: OnboardingGradientType { - onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default - } + private let onboardingManager: OnboardingAddToDockManaging init( delegate: OnboardingNavigationDelegate?, contextualOnboardingLogic: ContextualOnboardingLogic, onboardingPixelReporter: OnboardingPixelReporting, - onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging = OnboardingManager() + onboardingManager: OnboardingAddToDockManaging = OnboardingManager() ) { self.delegate = delegate self.contextualOnboardingLogic = contextualOnboardingLogic @@ -66,24 +62,24 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { private func createInitialDialog() -> some View { let viewModel = OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), delegate: delegate, pixelReporter: onboardingPixelReporter) - let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage + let message = UserText.Onboarding.ContextualOnboarding.onboardingTryASearchMessage return FadeInView { OnboardingTrySearchDialog(message: message, viewModel: viewModel) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) + .onboardingContextualBackgroundStyle(background: .illustratedGradient) .onFirstAppear { [weak self] in self?.onboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTrySearchUnique) } } private func createSubsequentDialog() -> some View { - let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteNTPTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), delegate: delegate, pixelReporter: onboardingPixelReporter) + let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteNTPTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), delegate: delegate, pixelReporter: onboardingPixelReporter) return FadeInView { OnboardingTryVisitingSiteDialog(logoPosition: .top, viewModel: viewModel) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) + .onboardingContextualBackgroundStyle(background: .illustratedGradient) .onFirstAppear { [weak self] in self?.onboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTryVisitSiteUnique) } @@ -91,11 +87,14 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { private func createAddFavoriteDialog(message: String) -> some View { FadeInView { - DaxDialogView(logoPosition: .top) { - ContextualDaxDialogContent(message: NSAttributedString(string: message)) + ScrollView(.vertical) { + DaxDialogView(logoPosition: .top) { + ContextualDaxDialogContent(message: NSAttributedString(string: message)) + } + .padding() } - .padding() } + .onboardingContextualBackgroundStyle(background: .illustratedGradient) } private func createFinalDialog(onDismiss: @escaping () -> Void) -> some View { @@ -105,8 +104,8 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { (UserText.AddToDockOnboarding.Promo.contextualMessage, UserText.AddToDockOnboarding.Buttons.startBrowsing) } else { ( - onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, - UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton + UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenMessage, + UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenButton ) } @@ -136,7 +135,7 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { dismissAction: dismissAction ) } - .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) + .onboardingContextualBackgroundStyle(background: .illustratedGradient) .onFirstAppear { [weak self] in self?.contextualOnboardingLogic.setFinalOnboardingDialogSeen() self?.onboardingPixelReporter.trackScreenImpression(event: .daxDialogsEndOfJourneyNewTabUnique) diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index 25f7b7542c..5ccdb1a462 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -48,18 +48,14 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { private let contextualOnboardingSettings: ContextualOnboardingSettings private let contextualOnboardingPixelReporter: OnboardingPixelReporting private let contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding - private let onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging - - private var gradientType: OnboardingGradientType { - onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default - } + private let onboardingManager: OnboardingAddToDockManaging init( contextualOnboardingLogic: ContextualOnboardingLogic, contextualOnboardingSettings: ContextualOnboardingSettings = DefaultDaxDialogsSettings(), contextualOnboardingPixelReporter: OnboardingPixelReporting, - contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding = OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), - onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging = OnboardingManager() + contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding = OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), + onboardingManager: OnboardingAddToDockManaging = OnboardingManager() ) { self.contextualOnboardingSettings = contextualOnboardingSettings self.contextualOnboardingLogic = contextualOnboardingLogic @@ -99,7 +95,7 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { let viewWithBackground = rootView .onboardingDaxDialogStyle() - .onboardingContextualBackgroundStyle(background: .gradientOnly(gradientType)) + .onboardingContextualBackgroundStyle(background: .gradientOnly) let hostingController = UIHostingController(rootView: AnyView(viewWithBackground)) if #available(iOS 16.0, *) { hostingController.sizingOptions = [.intrinsicContentSize] @@ -116,16 +112,12 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { ) -> some View { func dialogMessage() -> NSAttributedString { - if onboardingManager.isOnboardingHighlightsEnabled { - let message = UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage - let boldRange = message.range(of: "DuckDuckGo Search") - return message.attributed.with(attribute: .font, value: UIFont.daxBodyBold(), in: boldRange) - } else { - return UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage.attributed - } + let message = UserText.Onboarding.ContextualOnboarding.onboardingFirstSearchDoneMessage + let boldRange = message.range(of: "DuckDuckGo Search") + return message.attributed.with(attribute: .font, value: UIFont.daxBodyBold(), in: boldRange) } - let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: contextualOnboardingSiteSuggestionsProvider, delegate: delegate, pixelReporter: contextualOnboardingPixelReporter) + let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: contextualOnboardingSiteSuggestionsProvider, delegate: delegate, pixelReporter: contextualOnboardingPixelReporter) // If should not show websites search after searching inform the delegate that the user dimissed the dialog, otherwise let the dialog handle it. let gotItAction: () -> Void = if shouldFollowUpToWebsiteSearch { @@ -147,7 +139,7 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } private func tryVisitingSiteDialog(delegate: ContextualOnboardingDelegate) -> some View { - let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: contextualOnboardingSiteSuggestionsProvider, delegate: delegate, pixelReporter: contextualOnboardingPixelReporter) + let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.Onboarding.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: contextualOnboardingSiteSuggestionsProvider, delegate: delegate, pixelReporter: contextualOnboardingPixelReporter) return OnboardingTryVisitingSiteDialog(logoPosition: .left, viewModel: viewModel) .onFirstAppear { [weak self] in self?.contextualOnboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTryVisitSiteUnique) @@ -188,8 +180,8 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { (UserText.AddToDockOnboarding.Promo.contextualMessage, UserText.AddToDockOnboarding.Buttons.startBrowsing) } else { ( - onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, - UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton + UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenMessage, + UserText.Onboarding.ContextualOnboarding.onboardingFinalScreenButton ) } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift index 3aef1552a0..91b863d63a 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift @@ -47,15 +47,11 @@ final class ContextualOnboardingPresenter: ContextualOnboardingPresenting { } func presentContextualOnboarding(for spec: DaxDialogs.BrowsingSpec, in vc: TabViewOnboardingDelegate) { - if variantManager.isContextualDaxDialogsEnabled { - presentExperimentContextualOnboarding(for: spec, in: vc) - } else { - presentControlContextualOnboarding(for: spec, in: vc) - } + presentExperimentContextualOnboarding(for: spec, in: vc) } func dismissContextualOnboardingIfNeeded(from vc: TabViewOnboardingDelegate) { - guard variantManager.isContextualDaxDialogsEnabled, let daxContextualOnboarding = vc.daxContextualOnboardingController else { return } + guard let daxContextualOnboarding = vc.daxContextualOnboardingController else { return } remove(daxController: daxContextualOnboarding, fromParent: vc) } diff --git a/DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift b/DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift deleted file mode 100644 index 528ff47fc8..0000000000 --- a/DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// DefaultVariantManager+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 Foundation -import BrowserServicesKit - -extension VariantManager { - - var isOnboardingHighlightsExperiment: Bool { - isSupported(feature: .newOnboardingIntroHighlights) - } - - var isContextualDaxDialogsEnabled: Bool { - isSupported(feature: .contextualDaxDialogs) - } - -} diff --git a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift index b2f212410f..14d06eddbc 100644 --- a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift +++ b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift @@ -56,39 +56,6 @@ final class OnboardingManager { } } -// MARK: - Onboarding Highlights - -protocol OnboardingHighlightsManaging: AnyObject { - var isOnboardingHighlightsEnabled: Bool { get } -} - -protocol OnboardingHighlightsDebugging: OnboardingHighlightsManaging { - var isOnboardingHighlightsLocalFlagEnabled: Bool { get set } - var isOnboardingHighlightsFeatureFlagEnabled: Bool { get } -} - - -extension OnboardingManager: OnboardingHighlightsManaging, OnboardingHighlightsDebugging { - - var isOnboardingHighlightsEnabled: Bool { - variantManager.isOnboardingHighlightsExperiment || (isOnboardingHighlightsLocalFlagEnabled && isOnboardingHighlightsFeatureFlagEnabled) - } - - var isOnboardingHighlightsLocalFlagEnabled: Bool { - get { - appDefaults.onboardingHighlightsEnabled - } - set { - appDefaults.onboardingHighlightsEnabled = newValue - } - } - - var isOnboardingHighlightsFeatureFlagEnabled: Bool { - featureFlagger.isFeatureOn(.onboardingHighlights) - } - -} - // MARK: - Add to Dock protocol OnboardingAddToDockManaging: AnyObject { @@ -103,7 +70,14 @@ protocol OnboardingAddToDockDebugging: AnyObject { extension OnboardingManager: OnboardingAddToDockManaging, OnboardingAddToDockDebugging { var addToDockEnabledState: OnboardingAddToDockState { - // TODO: Add variant condition OR local conditions + // Check if the variant supports Add to Dock + if variantManager.isSupported(feature: .addToDockIntro) { + return .intro + } else if variantManager.isSupported(feature: .addToDockContextual) { + return .contextual + } + + // If the variant does not support Add to Dock check if it's enabled for internal users. guard isAddToDockFeatureFlagEnabled && isIphone else { return .disabled } return addToDockLocalFlagState diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift index 5e2e713a7e..f8e8c777ff 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift @@ -23,30 +23,14 @@ extension OnboardingIntroViewModel { struct Copy { let introTitle: String let browserComparisonTitle: String - let trackerBlockers: String - let cookiePopups: String - let creepyAds: String - let eraseBrowsingData: String } } extension OnboardingIntroViewModel.Copy { static let `default` = OnboardingIntroViewModel.Copy( - introTitle: UserText.DaxOnboardingExperiment.Intro.title, - browserComparisonTitle: UserText.DaxOnboardingExperiment.BrowsersComparison.title, - trackerBlockers: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers, - cookiePopups: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups, - creepyAds: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds, - eraseBrowsingData: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData + introTitle: UserText.Onboarding.Intro.title, + browserComparisonTitle: UserText.Onboarding.BrowsersComparison.title ) - static let highlights = OnboardingIntroViewModel.Copy( - introTitle: UserText.HighlightsOnboardingExperiment.Intro.title, - browserComparisonTitle: UserText.HighlightsOnboardingExperiment.BrowsersComparison.title, - trackerBlockers: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers, - cookiePopups: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups, - creepyAds: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds, - eraseBrowsingData: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData - ) } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index d16992322b..740e6659ca 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -26,12 +26,11 @@ final class OnboardingIntroViewModel: ObservableObject { @Published private(set) var state: OnboardingView.ViewState = .landing let copy: Copy - let gradientType: OnboardingGradientType var onCompletingOnboardingIntro: (() -> Void)? private var introSteps: [OnboardingIntroStep] private let pixelReporter: OnboardingIntroPixelReporting & OnboardingAddToDockReporting - private let onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging + private let onboardingManager: OnboardingAddToDockManaging private let isIpad: Bool private let urlOpener: URLOpener private let appIconProvider: () -> AppIcon @@ -39,7 +38,7 @@ final class OnboardingIntroViewModel: ObservableObject { init( pixelReporter: OnboardingIntroPixelReporting & OnboardingAddToDockReporting, - onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging = OnboardingManager(), + onboardingManager: OnboardingAddToDockManaging = OnboardingManager(), isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, urlOpener: URLOpener = UIApplication.shared, appIconProvider: @escaping () -> AppIcon = { AppIconManager.shared.appIcon }, @@ -52,16 +51,14 @@ final class OnboardingIntroViewModel: ObservableObject { self.appIconProvider = appIconProvider self.addressBarPositionProvider = addressBarPositionProvider - introSteps = if onboardingManager.isOnboardingHighlightsEnabled && onboardingManager.addToDockEnabledState == .intro { - isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsAddToDockIphoneFlow - } else if onboardingManager.isOnboardingHighlightsEnabled { - isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsIPhoneFlow + // Add to Dock experiment assigned only to iPhone users + introSteps = if onboardingManager.addToDockEnabledState == .intro { + OnboardingIntroStep.addToDockIphoneFlow } else { - OnboardingIntroStep.defaultFlow + isIpad ? OnboardingIntroStep.defaultIPadFlow : OnboardingIntroStep.defaultIPhoneFlow } - copy = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default - gradientType = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + copy = .default } func onAppear() { @@ -129,12 +126,7 @@ private extension OnboardingIntroViewModel { func makeViewState(for introStep: OnboardingIntroStep) -> OnboardingView.ViewState { func stepInfo() -> OnboardingView.ViewState.Intro.StepInfo { - guard - let currentStepIndex = introSteps.firstIndex(of: introStep), - onboardingManager.isOnboardingHighlightsEnabled - else { - return .hidden - } + guard let currentStepIndex = introSteps.firstIndex(of: introStep) else { return .hidden } // Remove startOnboardingDialog from the count of total steps since we don't show the progress for that step. return OnboardingView.ViewState.Intro.StepInfo(currentStep: currentStepIndex, totalSteps: introSteps.count - 1) @@ -157,14 +149,12 @@ private extension OnboardingIntroViewModel { } func handleSetDefaultBrowserAction() { - if onboardingManager.addToDockEnabledState == .intro && onboardingManager.isOnboardingHighlightsEnabled { + if onboardingManager.addToDockEnabledState == .intro { state = makeViewState(for: .addToDockPromo) pixelReporter.trackAddToDockPromoImpression() - } else if onboardingManager.isOnboardingHighlightsEnabled { + } else { state = makeViewState(for: .appIconSelection) pixelReporter.trackChooseAppIconImpression() - } else { - onCompletingOnboardingIntro?() } } @@ -179,8 +169,7 @@ private enum OnboardingIntroStep { case addressBarPositionSelection case addToDockPromo - static let defaultFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison] - static let highlightsIPhoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] - static let highlightsIPadFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection] - static let highlightsAddToDockIphoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .addToDockPromo, .appIconSelection, .addressBarPositionSelection] + static let defaultIPhoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] + static let defaultIPadFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection] + static let addToDockIphoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .addToDockPromo, .appIconSelection, .addressBarPositionSelection] } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift index 5ef61b133c..979593d0db 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift @@ -23,7 +23,6 @@ import Onboarding private enum Metrics { static let titleFont = Font.system(size: 20, weight: .semibold) - static let messageFont = Font.system(size: 16) } extension OnboardingView { @@ -51,7 +50,7 @@ extension OnboardingView { var body: some View { VStack(spacing: 16.0) { - AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AddressBarPosition.title, startAnimating: animateTitle) { + AnimatableTypingText(UserText.Onboarding.AddressBarPosition.title, startAnimating: animateTitle) { showContent.wrappedValue = true } .foregroundColor(.primary) @@ -61,7 +60,7 @@ extension OnboardingView { OnboardingAddressBarPositionPicker() Button(action: action) { - Text(verbatim: UserText.HighlightsOnboardingExperiment.AddressBarPosition.cta) + Text(verbatim: UserText.Onboarding.AddressBarPosition.cta) } .buttonStyle(PrimaryButtonStyle()) } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift index a85d4df605..5fd7a30be0 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift @@ -50,13 +50,13 @@ extension OnboardingView { var body: some View { VStack(spacing: 16.0) { - AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AppIconSelection.title, startAnimating: animateTitle) { + AnimatableTypingText(UserText.Onboarding.AppIconSelection.title, startAnimating: animateTitle) { animateMessage.wrappedValue = true } .foregroundColor(.primary) .font(Metrics.titleFont) - AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AppIconSelection.message, startAnimating: animateMessage) { + AnimatableTypingText(UserText.Onboarding.AppIconSelection.message, startAnimating: animateMessage) { withAnimation { showContent.wrappedValue = true } @@ -68,7 +68,7 @@ extension OnboardingView { AppIconPicker() Button(action: action) { - Text(UserText.HighlightsOnboardingExperiment.AppIconSelection.cta) + Text(UserText.Onboarding.AppIconSelection.cta) } .buttonStyle(PrimaryButtonStyle()) } @@ -83,5 +83,4 @@ extension OnboardingView { private enum Metrics { static let titleFont = Font.system(size: 20, weight: .semibold) static let messageFont = Font.system(size: 16) - static let pickerLeadingOffset: CGFloat = -20 } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift index 0772d9e3b8..fdaf12a120 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift @@ -61,7 +61,7 @@ extension OnboardingView { OnboardingActions( viewModel: .init( - primaryButtonTitle: UserText.DaxOnboardingExperiment.BrowsersComparison.cta, + primaryButtonTitle: UserText.Onboarding.BrowsersComparison.cta, secondaryButtonTitle: UserText.onboardingSkip ), primaryAction: setAsDefaultBrowserAction, diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift index 430be926ea..d4324609f6 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift @@ -39,7 +39,7 @@ extension OnboardingView { var body: some View { VStack(spacing: 24.0) { - AnimatableTypingText(title) { + AnimatableTypingText(title, startAnimating: animateText) { withAnimation { showCTA.wrappedValue = true } @@ -48,7 +48,7 @@ extension OnboardingView { .font(Font.system(size: 20, weight: .bold)) Button(action: action) { - Text(UserText.DaxOnboardingExperiment.Intro.cta) + Text(UserText.Onboarding.Intro.cta) } .buttonStyle(PrimaryButtonStyle()) .visibility(showCTA.wrappedValue ? .visible : .invisible) diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 2d99d818be..38d7d37a3b 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -40,7 +40,6 @@ struct OnboardingView: View { @State private var appIconPickerContentState = AppIconPickerContentState() @State private var addressBarPositionContentState = AddressBarPositionContentState() - @State private var addToDockPromoContentState = AddToDockPromoContentState() init(model: OnboardingIntroViewModel) { self.model = model @@ -57,7 +56,6 @@ struct OnboardingView: View { onboardingDialogView(state: viewState) } } - .onboardingGradient(model.gradientType) } private func onboardingDialogView(state: ViewState.Intro) -> some View { @@ -76,10 +74,6 @@ struct OnboardingView: View { case .browsersComparisonDialog: showComparisonButton = true animateComparisonText = false - case .addToDockPromoDialog: - addToDockPromoContentState.animateTitle = false - addToDockPromoContentState.animateMessage = false - addToDockPromoContentState.showContent = true case .chooseAppIconDialog: appIconPickerContentState.animateTitle = false appIconPickerContentState.animateMessage = false diff --git a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift index 881e5fd721..09efd90925 100644 --- a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift +++ b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift @@ -38,12 +38,11 @@ extension OnboardingStyles { func body(content: Content) -> some View { ZStack { switch backgroundType { - case let .illustratedGradient(gradientType): + case .illustratedGradient: OnboardingBackground() - .onboardingGradient(gradientType) .ignoresSafeArea(.keyboard) - case let .gradientOnly(gradientType): - OnboardingGradientView(type: gradientType) + case .gradientOnly: + OnboardingGradientView() .ignoresSafeArea(.keyboard) } @@ -71,25 +70,6 @@ extension View { } enum OnboardingBackgroundType { - case illustratedGradient(OnboardingGradientType) - case gradientOnly(OnboardingGradientType) -} - -enum OnboardingGradientTypeKey: EnvironmentKey { - static var defaultValue: OnboardingGradientType = .default -} - -extension EnvironmentValues { - var onboardingGradientType: OnboardingGradientType { - get { self[OnboardingGradientTypeKey.self] } - set { self[OnboardingGradientTypeKey.self] = newValue } - } -} - -extension View { - - func onboardingGradient(_ type: OnboardingGradientType) -> some View { - environment(\.onboardingGradientType, type) - } - + case illustratedGradient + case gradientOnly } diff --git a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift index ed71123a3c..fa8c180ded 100644 --- a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift +++ b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift @@ -24,31 +24,17 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding private static let imageSearch = "!image " private let countryAndLanguageProvider: OnboardingRegionAndLanguageProvider - private let onboardingManager: OnboardingHighlightsManaging - init( - countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current, - onboardingManager: OnboardingHighlightsManaging = OnboardingManager() - ) { + init(countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current) { self.countryAndLanguageProvider = countryAndLanguageProvider - self.onboardingManager = onboardingManager } var list: [ContextualOnboardingListItem] { - if onboardingManager.isOnboardingHighlightsEnabled { - [ - option1, - option2, - surpriseMe - ] - } else { - [ - option1, - option2, - option3, - surpriseMe - ] - } + [ + option1, + option2, + surpriseMe + ] } private var country: String? { @@ -61,9 +47,9 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding private var option1: ContextualOnboardingListItem { var search: String if language == "en" { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption1English + search = UserText.Onboarding.ContextualOnboarding.tryASearchOption1English } else { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption1International + search = UserText.Onboarding.ContextualOnboarding.tryASearchOption1International } return ContextualOnboardingListItem.search(title: search) } @@ -73,28 +59,17 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding // ISO 3166-1 Region capitalized. // https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html if isUSCountry { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption2English + search = UserText.Onboarding.ContextualOnboarding.tryASearchOption2English } else { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption2International + search = UserText.Onboarding.ContextualOnboarding.tryASearchOption2International } return ContextualOnboardingListItem.search(title: search) } - private var option3: ContextualOnboardingListItem { - let search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption3 - return ContextualOnboardingListItem.search(title: search) - } - private var surpriseMe: ContextualOnboardingListItem { - let search = if onboardingManager.isOnboardingHighlightsEnabled { - Self.imageSearch + UserText.HighlightsOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMe - } else { - isUSCountry ? - UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish : - UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeInternational - } + let search = Self.imageSearch + UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMe - return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.Onboarding.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) } private var isUSCountry: Bool { diff --git a/DuckDuckGo/TabSwitcherViewController.swift b/DuckDuckGo/TabSwitcherViewController.swift index 585d7dcafb..617ea6054c 100644 --- a/DuckDuckGo/TabSwitcherViewController.swift +++ b/DuckDuckGo/TabSwitcherViewController.swift @@ -331,18 +331,8 @@ class TabSwitcherViewController: UIViewController { } Pixel.fire(pixel: .forgetAllPressedTabSwitching) - let isNewOnboarding = DefaultVariantManager().isContextualDaxDialogsEnabled - - if !isNewOnboarding - && DaxDialogs.shared.shouldShowFireButtonPulse { - let spec = DaxDialogs.shared.fireButtonEducationMessage() - performSegue(withIdentifier: "ActionSheetDaxDialog", sender: spec) - } else { - if isNewOnboarding { - ViewHighlighter.hideAll() - } - presentForgetDataAlert() - } + ViewHighlighter.hideAll() + presentForgetDataAlert() } private func forgetAll() { diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 24dddd1628..bc248cd104 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -1587,9 +1587,6 @@ extension TabViewController: WKNavigationDelegate { return } - if !DefaultVariantManager().isContextualDaxDialogsEnabled { - isShowingFullScreenDaxDialog = true - } scheduleTrackerNetworksAnimation(collapsing: !spec.highlightAddressBar) let daxDialogSourceURL = self.url diff --git a/DuckDuckGo/TabsBarViewController.swift b/DuckDuckGo/TabsBarViewController.swift index a2204f467d..494d5da197 100644 --- a/DuckDuckGo/TabsBarViewController.swift +++ b/DuckDuckGo/TabsBarViewController.swift @@ -104,16 +104,8 @@ class TabsBarViewController: UIViewController { self.present(controller: alert, fromView: fireButton) } - if DefaultVariantManager().isContextualDaxDialogsEnabled { - delegate?.tabsBarDidRequestFireEducationDialog(self) - showClearDataAlert() - } else { - if DaxDialogs.shared.shouldShowFireButtonPulse { - delegate?.tabsBarDidRequestFireEducationDialog(self) - } else { - showClearDataAlert() - } - } + delegate?.tabsBarDidRequestFireEducationDialog(self) + showClearDataAlert() } @IBAction func onNewTabPressed() { @@ -322,14 +314,8 @@ extension MainViewController: TabsBarDelegate { } func tabsBarDidRequestFireEducationDialog(_ controller: TabsBarViewController) { - if DefaultVariantManager().isContextualDaxDialogsEnabled { - currentTab?.dismissContextualDaxFireDialog() - ViewHighlighter.hideAll() - } else { - if let spec = DaxDialogs.shared.fireButtonEducationMessage() { - segueToActionSheetDaxDialogWithSpec(spec) - } - } + currentTab?.dismissContextualDaxFireDialog() + ViewHighlighter.hideAll() } func tabsBarDidRequestTabSwitcher(_ controller: TabsBarViewController) { diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index ff3430d987..30a8483fbb 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1353,61 +1353,20 @@ AI Chat is an optional feature available at [duck.ai](ddgquicklink://duck.ai) th public static let newTabPageIntroMessageBody = NSLocalizedString("new.tab.page.intro.message.body", value: "Customize your Favorites and go-to features. Reorder things or hide them to keep it clean.", comment: "Information message about New Tab Page redesign") - // 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") + // MARK: - Onboarding + public enum Onboarding { + + public enum Intro { + public static let title = NSLocalizedString("onboarding.highlights.intro.title", value: "Hi there.\n\nReady for a faster browser that keeps you protected?", 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 enum BrowsersComparison { + public static let title = NSLocalizedString("onboarding.highlights.browsers.title", value: "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 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 how the browser allows you to block 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 of swiftly erase browsing data") - } - } - - enum ContextualOnboarding { - static let onboardingTryASearchTitle = NSLocalizedString("contextual.onboarding.try-a-search.title", value: "Ready to get started?\nTry a search!", comment: "Title of a popover on the browser that invites the user to try a search") - static let onboardingTryASearchMessage = NSLocalizedString("contextual.onboarding.try-a-search.message", value: "Your DuckDuckGo searches are always anonymous.", comment: "Message of a popover on the browser that invites the user to try a search explaining that their searches are anonymous") - static let onboardingTryASiteTitle = NSLocalizedString("contextual.onboarding.try-a-site.title", value: "Next, try visiting a site!", comment: "Title of a popover on the browser that invites the user to try a visiting a website") - static let onboardingTryASiteNTPTitle = NSLocalizedString("contextual.onboarding.ntp.try-a-site.title", value: "Try visiting a site!", comment: "Title of a popover on the new tab page browser that invites the user to try a visiting a website") - static let onboardingTryASiteMessage = NSLocalizedString("contextual.onboarding.try-a-site.message", value: "I’ll block trackers so they can’t spy on you.", comment: "Message of a popover on the browser that invites the user to try visiting a website to explain that we block trackers") - static let onboardingTryFireButtonMessage = NSLocalizedString("contextual.onboarding.try-fire-button.message", value: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥", comment: "Message of a popover on the browser that invites the user to try visiting the browser Fire Button. Please leave the line break") - static let onboardingGotItButton = NSLocalizedString("contextual.onboarding.got-it.button", value: "Got it!", comment: "During onboarding steps this button is shown and takes either to the next steps or closes the onboarding.") - static let onboardingFirstSearchDoneMessage = NSLocalizedString("contextual.onboarding.first-search-done.message", value: "That’s DuckDuckGo Search. Private. Fast. Fewer ads.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") - static let onboardingFinalScreenTitle = NSLocalizedString("contextual.onboarding.final-screen.title", value: "You’ve got this!", comment: "Title of the last screen of the onboarding to the browser app") - static let onboardingFinalScreenMessage = NSLocalizedString("contextual.onboarding.final-screen.message", value: "Remember: every time you browse with me a creepy ad loses its wings. 👌", comment: "Message of the last screen of the onboarding to the browser app.") - static let onboardingFinalScreenButton = NSLocalizedString("contextual.onboarding.final-screen.button", value: "High five!", comment: "Button on the last screen of the onboarding, it will dismiss the onboarding screen.") - static let tryASearchOption1English = NSLocalizedString("contextual.onboarding.try-search.option1-English", value: "how to say “duck” in spanish", comment: "Browser Search query for how to say duck in english") - static let tryASearchOption1International = NSLocalizedString("contextual.onboarding.try-search.option1international", value: "how to say “duck” in english", comment: "Browser Search query for how to say duck in english") - static let tryASearchOption2English = NSLocalizedString("contextual.onboarding.try-search.option2-english", value: "mighty ducks cast", comment: "Search query for the cast of Mighty Ducks") - static let tryASearchOption2International = NSLocalizedString("contextual.onboarding.try-search.option2-international", value: "cast of avatar", comment: "Search query for the cast of Avatar") - static let tryASearchOption3 = NSLocalizedString("contextual.onboarding.try-search.option3", value: "local weather", comment: "Browser Search query for local weather") - static let tryASearchOptionSurpriseMeTitle = NSLocalizedString("contextual.onboarding.try-search.surprise-me-title", value: "Surprise me!", comment: "Title for a button that triggers an unknown search query for the user.") - static let tryASearchOptionSurpriseMeEnglish = NSLocalizedString("contextual.onboarding.try-search.surprise-me-english", value: "chocolate chip cookie recipes", comment: "Browser Search query for chocolate chip cookie recipes") - static let tryASearchOptionSurpriseMeInternational = NSLocalizedString("contextual.onboarding.try-search.surprise-me-international", value: "dinner recipes", comment: "Browser Search query for dinner recipes") - - static let daxDialogBrowsingWithOneTracker = NSLocalizedString("contextual.onboarding.browsing.one.tracker", value: "*%1$@* was trying to track you here. I blocked them!\n\n☝️ Tap the shield for more info.", comment: "Parameter is domain name (string)") - static let daxDialogBrowsingWithMultipleTrackers = NSLocalizedString("contextual.onboarding.browsing.multiple.trackers", comment: "First parameter is a count of additional trackers, second and third are names of the tracker networks (strings)") - } - } - - public enum HighlightsOnboardingExperiment { - enum Intro { - public static let title = NSLocalizedString("onboarding.highlights.intro.title", value: "Hi there.\n\nReady for a faster browser that keeps you protected?", comment: "The title of the onboarding dialog popup") - } - - enum BrowsersComparison { - public static let title = NSLocalizedString("onboarding.highlights.browsers.title", value: "Protections activated!", comment: "The title of the dialog to show the privacy features that DuckDuckGo offers") - - enum Features { public static let trackerBlockers = NSLocalizedString("onboarding.highlights.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.highlights.browsers.features.cookiePopups.title", value: "Block cookie requests & popups", comment: "Message to highlight how the browser allows you to block cookie pop-ups") public static let creepyAds = NSLocalizedString("onboarding.highlights.browsers.features.creepyAds.title", value: "Block targeted ads", comment: "Message to highlight browser capability of blocking creepy ads") @@ -1432,10 +1391,26 @@ AI Chat is an optional feature available at [duck.ai](ddgquicklink://duck.ai) th } enum ContextualOnboarding { + static let onboardingTryASearchTitle = NSLocalizedString("contextual.onboarding.try-a-search.title", value: "Ready to get started?\nTry a search!", comment: "Title of a popover on the browser that invites the user to try a search") static let onboardingTryASearchMessage = NSLocalizedString("contextual.onboarding.highlights.try-a-search.message", value: "Your DuckDuckGo searches are always private.", comment: "Message of a popover on the browser that invites the user to try a search explaining that their searches are private") static let onboardingFirstSearchDoneMessage = NSLocalizedString("contextual.onboarding.highlights.first-search-done.message", value: "That’s DuckDuckGo Search! Private. Fast. Fewer ads.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") + static let onboardingTryASiteTitle = NSLocalizedString("contextual.onboarding.try-a-site.title", value: "Next, try visiting a site!", comment: "Title of a popover on the browser that invites the user to try a visiting a website") static let onboardingFinalScreenMessage = NSLocalizedString("contextual.onboarding.highlights.final-screen.message", value: "Remember: every time you browse with me a creepy ad loses its wings.", comment: "Message of the last screen of the onboarding to the browser app.") static let tryASearchOptionSurpriseMe = NSLocalizedString("contextual.onboarding.highlights.try-search.surprise-me", value: "baby ducklings", comment: "Browser Search query for baby ducklings") + static let onboardingTryASiteNTPTitle = NSLocalizedString("contextual.onboarding.ntp.try-a-site.title", value: "Try visiting a site!", comment: "Title of a popover on the new tab page browser that invites the user to try a visiting a website") + static let onboardingTryASiteMessage = NSLocalizedString("contextual.onboarding.try-a-site.message", value: "I’ll block trackers so they can’t spy on you.", comment: "Message of a popover on the browser that invites the user to try visiting a website to explain that we block trackers") + static let onboardingTryFireButtonMessage = NSLocalizedString("contextual.onboarding.try-fire-button.message", value: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥", comment: "Message of a popover on the browser that invites the user to try visiting the browser Fire Button. Please leave the line break") + static let onboardingGotItButton = NSLocalizedString("contextual.onboarding.got-it.button", value: "Got it!", comment: "During onboarding steps this button is shown and takes either to the next steps or closes the onboarding.") + static let onboardingFinalScreenTitle = NSLocalizedString("contextual.onboarding.final-screen.title", value: "You’ve got this!", comment: "Title of the last screen of the onboarding to the browser app") + static let onboardingFinalScreenButton = NSLocalizedString("contextual.onboarding.final-screen.button", value: "High five!", comment: "Button on the last screen of the onboarding, it will dismiss the onboarding screen.") + static let tryASearchOption1English = NSLocalizedString("contextual.onboarding.try-search.option1-English", value: "how to say “duck” in spanish", comment: "Browser Search query for how to say duck in english") + static let tryASearchOption1International = NSLocalizedString("contextual.onboarding.try-search.option1international", value: "how to say “duck” in english", comment: "Browser Search query for how to say duck in english") + static let tryASearchOption2English = NSLocalizedString("contextual.onboarding.try-search.option2-english", value: "mighty ducks cast", comment: "Search query for the cast of Mighty Ducks") + static let tryASearchOption2International = NSLocalizedString("contextual.onboarding.try-search.option2-international", value: "cast of avatar", comment: "Search query for the cast of Avatar") + static let tryASearchOptionSurpriseMeTitle = NSLocalizedString("contextual.onboarding.try-search.surprise-me-title", value: "Surprise me!", comment: "Title for a button that triggers an unknown search query for the user.") + + static let daxDialogBrowsingWithOneTracker = NSLocalizedString("contextual.onboarding.browsing.one.tracker", value: "*%1$@* was trying to track you here. I blocked them!\n\n☝️ Tap the shield for more info.", comment: "Parameter is domain name (string)") + static let daxDialogBrowsingWithMultipleTrackers = NSLocalizedString("contextual.onboarding.browsing.multiple.trackers", comment: "First parameter is a count of additional trackers, second and third are names of the tracker networks (strings)") } enum FireDialog { diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index f65a3bac1a..79794bf690 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -828,15 +828,9 @@ /* Button on the last screen of the onboarding, it will dismiss the onboarding screen. */ "contextual.onboarding.final-screen.button" = "High five!"; -/* Message of the last screen of the onboarding to the browser app. */ -"contextual.onboarding.final-screen.message" = "Remember: every time you browse with me a creepy ad loses its wings. 👌"; - /* Title of the last screen of the onboarding to the browser app */ "contextual.onboarding.final-screen.title" = "You’ve got this!"; -/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ -"contextual.onboarding.first-search-done.message" = "That’s DuckDuckGo Search. Private. Fast. Fewer ads."; - /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Got it!"; @@ -855,9 +849,6 @@ /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Try visiting a site!"; -/* Message of a popover on the browser that invites the user to try a search explaining that their searches are anonymous */ -"contextual.onboarding.try-a-search.message" = "Your DuckDuckGo searches are always anonymous."; - /* Title of a popover on the browser that invites the user to try a search */ "contextual.onboarding.try-a-search.title" = "Ready to get started?\nTry a search!"; @@ -882,15 +873,6 @@ /* Search query for the cast of Avatar */ "contextual.onboarding.try-search.option2-international" = "cast of avatar"; -/* Browser Search query for local weather */ -"contextual.onboarding.try-search.option3" = "local weather"; - -/* Browser Search query for chocolate chip cookie recipes */ -"contextual.onboarding.try-search.surprise-me-english" = "chocolate chip cookie recipes"; - -/* Browser Search query for dinner recipes */ -"contextual.onboarding.try-search.surprise-me-international" = "dinner recipes"; - /* Title for a button that triggers an unknown search query for the user. */ "contextual.onboarding.try-search.surprise-me-title" = "Surprise me!"; @@ -1899,24 +1881,9 @@ https://duckduckgo.com/mac"; /* Button to change the default browser */ "onboarding.browsers.cta" = "Choose Your Browser"; -/* Message to highlight how the browser allows you to block 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 of swiftly 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!"; - /* The message of the option to set the address bar to the bottom. */ "onboarding.highlights.addressBarPosition.bottom.message" = "Easy to reach"; @@ -1971,9 +1938,6 @@ https://duckduckgo.com/mac"; /* 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/DuckDuckGoTests/BrowserComparisonModelTests.swift b/DuckDuckGoTests/BrowserComparisonModelTests.swift index 846c986f20..5dcbc51631 100644 --- a/DuckDuckGoTests/BrowserComparisonModelTests.swift +++ b/DuckDuckGoTests/BrowserComparisonModelTests.swift @@ -21,126 +21,46 @@ import XCTest @testable import DuckDuckGo final class BrowserComparisonModelTests: XCTestCase { - private var onboardingManager: OnboardingManagerMock! - - override func setUpWithError() throws { - try super.setUpWithError() - onboardingManager = OnboardingManagerMock() - } - - override func tearDownWithError() throws { - onboardingManager = nil - try super.tearDownWithError() - } - - func testWhenIsNotHighlightsThenBrowserComparisonFeaturePrivateSearchIsCorrect() throws { - // GIVEN - try [false, true].forEach { isOnboardingHighlightsEnabled in - onboardingManager.isOnboardingHighlightsEnabled = isOnboardingHighlightsEnabled - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - - // WHEN - let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .privateSearch })?.type.title) - - // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.privateSearch) - } - } - - func testWhenIsNotHighlightsThenBrowserComparisonFeatureBlockThirdPartyTrackersIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = false - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + func testBrowserComparisonFeaturePrivateSearchIsCorrect() throws { // WHEN - let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockThirdPartyTrackers })?.type.title) + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .privateSearch })?.type.title) // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers) - } - - func testWhenIsHighlightsThenBrowserComparisonFeatureBlockThirdPartyTrackersIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + XCTAssertEqual(result, UserText.Onboarding.BrowsersComparison.Features.privateSearch) - // WHEN - let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockThirdPartyTrackers })?.type.title) - - // THEN - XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers) } - func testWhenIsNotHighlightsThenBrowserComparisonFeatureBlockCookiePopupsIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = false - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - + func testBrowserComparisonFeatureBlockThirdPartyTrackersIsCorrect() throws { // WHEN - let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCookiePopups })?.type.title) + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockThirdPartyTrackers })?.type.title) // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups) + XCTAssertEqual(result, UserText.Onboarding.BrowsersComparison.Features.trackerBlockers) } - func testWhenIsHighlightsThenBrowserComparisonFeatureBlockCookiePopupsIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - + func testBrowserComparisonFeatureBlockCookiePopupsIsCorrect() throws { // WHEN let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCookiePopups })?.type.title) // THEN - XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups) - } - - func testWhenIsNotHighlightsThenBrowserComparisonFeatureBlockCreepyAdsIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = false - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - - // WHEN - let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCreepyAds })?.type.title) - - // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds) + XCTAssertEqual(result, UserText.Onboarding.BrowsersComparison.Features.cookiePopups) } - func testWhenIsHighlightsThenBrowserComparisonFeatureBlockCreepyAdsIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - + func testBrowserComparisonFeatureBlockCreepyAdsIsCorrect() throws { // WHEN let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCreepyAds })?.type.title) // THEN - XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds) + XCTAssertEqual(result, UserText.Onboarding.BrowsersComparison.Features.creepyAds) } func testWhenIsNotHighlightsThenBrowserComparisonFeatureEraseBrowsingDataIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = false - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - - // WHEN - let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .eraseBrowsingData })?.type.title) - - // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData) - } - - func testWhenIsHighlightsThenBrowserComparisonFeatureEraseBrowsingDataIsCorrect() throws { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true - BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager - // WHEN let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .eraseBrowsingData })?.type.title) // THEN - XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData) + XCTAssertEqual(result, UserText.Onboarding.BrowsersComparison.Features.eraseBrowsingData) } } diff --git a/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift b/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift index 5220b6dff9..c27f8caa2d 100644 --- a/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift +++ b/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift @@ -34,36 +34,9 @@ final class ContextualOnboardingPresenterTests: XCTestCase { try super.tearDownWithError() } - - func testWhenPresentContextualOnboardingAndVariantDoesNotSupportContextualDaxDialogsThenOldContextualOnboardingIsPresented() throws { + func testWhenPresentContextualOnboardingThenNewContextualOnboardingIsPresented() { // GIVEN - var variantManagerMock = MockVariantManager() - variantManagerMock.isSupportedBlock = { feature in - feature != .contextualDaxDialogs - } - let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) - let parent = TabViewControllerMock() - XCTAssertFalse(parent.didCallPerformSegue) - XCTAssertNil(parent.capturedSegueIdentifier) - XCTAssertNil(parent.capturedSender) - - // WHEN - sut.presentContextualOnboarding(for: .afterSearch, in: parent) - - // THEN - XCTAssertTrue(parent.didCallPerformSegue) - XCTAssertEqual(parent.capturedSegueIdentifier, "DaxDialog") - let sender = try XCTUnwrap(parent.capturedSender as? DaxDialogs.BrowsingSpec) - XCTAssertEqual(sender, DaxDialogs.BrowsingSpec.afterSearch) - } - - func testWhenPresentContextualOnboardingAndVariantSupportsContextualDaxDialogsThenThenNewContextualOnboardingIsPresented() { - // GIVEN - var variantManagerMock = MockVariantManager() - variantManagerMock.isSupportedBlock = { feature in - feature == .contextualDaxDialogs - } - let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) + let sut = ContextualOnboardingPresenter(variantManager: MockVariantManager(), daxDialogsFactory: contextualDaxDialogsFactory) let parent = TabViewControllerMock() XCTAssertFalse(parent.didCallAddChild) XCTAssertNil(parent.capturedChild) @@ -78,12 +51,8 @@ final class ContextualOnboardingPresenterTests: XCTestCase { func testWhenPresentContextualOnboardingForFireEducational_andBarAtTheTop_TheMessageHandPointsInTheRightDirection() throws { // GIVEN - var variantManagerMock = MockVariantManager() - variantManagerMock.isSupportedBlock = { feature in - feature == .contextualDaxDialogs - } let appSettings = AppSettingsMock() - let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory, appSettings: appSettings) + let sut = ContextualOnboardingPresenter(variantManager: MockVariantManager(), daxDialogsFactory: contextualDaxDialogsFactory, appSettings: appSettings) let parent = TabViewControllerMock() // WHEN @@ -96,13 +65,9 @@ final class ContextualOnboardingPresenterTests: XCTestCase { func testWhenPresentContextualOnboardingForFireEducational_andBarAtTheBottom_TheMessageHandPointsInTheRightDirection() throws { // GIVEN - var variantManagerMock = MockVariantManager() - variantManagerMock.isSupportedBlock = { feature in - feature == .contextualDaxDialogs - } let appSettings = AppSettingsMock() appSettings.currentAddressBarPosition = .bottom - let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory, appSettings: appSettings) + let sut = ContextualOnboardingPresenter(variantManager: MockVariantManager(), daxDialogsFactory: contextualDaxDialogsFactory, appSettings: appSettings) let parent = TabViewControllerMock() // WHEN @@ -113,14 +78,10 @@ final class ContextualOnboardingPresenterTests: XCTestCase { XCTAssertTrue(view.message.string.contains("👇")) } - func testWhenDismissContextualOnboardingAndVariantSupportsContextualDaxDialogsThenContextualOnboardingIsDismissed() { + func testWhenDismissContextualOnboardingThenContextualOnboardingIsDismissed() { // GIVEN let expectation = self.expectation(description: #function) - var variantManagerMock = MockVariantManager() - variantManagerMock.isSupportedBlock = { feature in - feature == .contextualDaxDialogs - } - let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) + let sut = ContextualOnboardingPresenter(variantManager: MockVariantManager(), daxDialogsFactory: contextualDaxDialogsFactory) let parent = TabViewControllerMock() let daxController = DaxContextualOnboardingControllerMock() daxController.removeFromParentExpectation = expectation @@ -140,29 +101,6 @@ final class ContextualOnboardingPresenterTests: XCTestCase { XCTAssertFalse(parent.daxDialogsStackView.arrangedSubviews.contains(daxController.view)) } - func testWhenDismissContextualOnboardingAndVariantDoesNotSupportsContextualDaxDialogsThenNothingHappens() { - // GIVEN - let expectation = self.expectation(description: #function) - expectation.isInverted = true - var variantManagerMock = MockVariantManager() - variantManagerMock.isSupportedBlock = { feature in - feature != .contextualDaxDialogs - } - let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) - let parent = TabViewControllerMock() - let daxController = DaxContextualOnboardingControllerMock() - daxController.removeFromParentExpectation = expectation - parent.daxContextualOnboardingController = daxController - XCTAssertFalse(daxController.didCallRemoveFromParent) - - // WHEN - sut.dismissContextualOnboardingIfNeeded(from: parent) - - // THEN - waitForExpectations(timeout: 0.4) - XCTAssertFalse(daxController.didCallRemoveFromParent) - } - } final class TabViewControllerMock: UIViewController, TabViewOnboardingDelegate { diff --git a/DuckDuckGoTests/DaxDialogTests.swift b/DuckDuckGoTests/DaxDialogTests.swift index 7a512f5cae..dab3924ac5 100644 --- a/DuckDuckGoTests/DaxDialogTests.swift +++ b/DuckDuckGoTests/DaxDialogTests.swift @@ -71,34 +71,17 @@ final class DaxDialog: XCTestCase { setupUserDefault(with: #file) entityProvider = MockEntityProvider() } - - func testWhenResumingRegularFlowThenNextHomeMessageIsBlankUntilBrowsingMessagesShown() { - mockVariantManager.isSupportedReturns = false - onboarding.enableAddFavoriteFlow() - onboarding.resumeRegularFlow() - XCTAssertNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.google))) - XCTAssertEqual(onboarding.nextHomeScreenMessage(), .final) - XCTAssertEqual(settings.homeScreenMessagesSeen, 2) - } func testWhenStartingAddFavoriteFlowThenNextMessageIsAddFavorite() { + // WHEN onboarding.enableAddFavoriteFlow() - XCTAssertEqual(onboarding.nextHomeScreenMessage(), .addFavorite) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - XCTAssertTrue(onboarding.isAddFavoriteFlow) - } - func testWhenStartingNextMessageAndAddFavoriteFlowThenNextHomeScreenMessagesSeenDoesNotIncrement() { - XCTAssertNotNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - onboarding.enableAddFavoriteFlow() - XCTAssertEqual(onboarding.nextHomeScreenMessage(), .addFavorite) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) + // THEN + XCTAssertEqual(onboarding.nextHomeScreenMessageNew(), .addFavorite) + XCTAssertTrue(onboarding.isAddFavoriteFlow) } - func testWhenEachVersionOfTrackersMessageIsShownThenFormattedCorrectlyAndNotShownAgain() { + func testWhenEachVersionOfTrackersMessageIsShownThenFormattedCorrectly() { 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), @@ -124,172 +107,9 @@ final class DaxDialog: XCTestCase { // Assert the expected case XCTAssertEqual(testCase.expected, onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo), line: UInt(testCase.line)) - - // Also assert the we don't see the message on subsequent calls - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo), line: UInt(testCase.line)) } } - - func testWhenTrackersShownThenFireEducationShown() { - let privacyInfo = makePrivacyInfo(url: URLs.example) - privacyInfo.trackerInfo.addDetectedTracker(detectedTrackerFrom(URLs.google, pageUrl: URLs.example.absoluteString), - onPageWithURL: URLs.example) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - } - - func testWhenMajorTrackerShownThenFireEducationShown() { - let privacyInfo = makePrivacyInfo(url: URLs.google) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - } - - func testWhenSearchShownThenNoTrackersIsShown() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg))) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - } - - func testWhenMajorTrackerShownThenNoTrackersIsNotShown() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.facebook))) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - } - - func testWhenTrackersShownThenNoTrackersIsNotShown() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.amazon))) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - } - - func testWhenMajorTrackerShownThenOwnedByIsNotShown() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.facebook))) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ownedByFacebook))) - } - - func testWhenSecondTimeOnSiteThatIsOwnedByFacebookThenShowNothingAfterFireEducation() { - let privacyInfo = makePrivacyInfo(url: URLs.ownedByFacebook) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenFirstTimeOnSiteThatIsOwnedByFacebookThenShowOwnedByMajorTrackingMessage() { - let privacyInfo = makePrivacyInfo(url: URLs.ownedByFacebook) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.BrowsingSpec.siteOwnedByMajorTracker.format(args: "instagram.com", "Facebook", 39.0), - onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenSecondTimeOnSiteThatIsMajorTrackerThenShowNothingAfterFireEducation() { - let privacyInfo = makePrivacyInfo(url: URLs.facebook) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenFirstTimeOnFacebookThenShowMajorTrackingMessage() { - let privacyInfo = makePrivacyInfo(url: URLs.facebook) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.BrowsingSpec.siteIsMajorTracker.format(args: "Facebook", URLs.facebook.host ?? ""), - onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenFirstTimeOnGoogleThenShowMajorTrackingMessage() { - let privacyInfo = makePrivacyInfo(url: URLs.google) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.BrowsingSpec.siteIsMajorTracker.format(args: "Google", URLs.google.host ?? ""), - onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenSecondTimeOnPageWithNoTrackersThenTrackersThenShowFireEducation() { - let privacyInfo = makePrivacyInfo(url: URLs.example) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenFirstTimeOnPageWithNoTrackersThenTrackersThenShowNoTrackersMessage() { - let privacyInfo = makePrivacyInfo(url: URLs.example) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.BrowsingSpec.withoutTrackers, onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenSecondTimeOnSearchPageThenShowNothing() { - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg))) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg))) - } - - func testWhenFirstTimeOnSearchPageThenShowSearchPageMessage() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.BrowsingSpec.afterSearch, onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg))) - } - - func testWhenOnSamePageAndPresenceOfTrackersChangesThenShowOnlyOneMessage() { - let privacyInfo = makePrivacyInfo(url: URLs.example) - XCTAssertEqual(DaxDialogs.BrowsingSpec.withoutTrackers, onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - - let privacyInfoWithTrackers = makePrivacyInfo(url: URLs.google) - privacyInfo.trackerInfo = privacyInfoWithTrackers.trackerInfo - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: privacyInfo)) - } - - func testWhenDimissedThenShowNothing() { - onboarding.dismiss() - XCTAssertNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 0) - XCTAssertNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - } - - func testWhenThirdTimeOnHomeScreenAndFireEducationSeenThenShowNothing() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.HomeScreenSpec.final, onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 2) - XCTAssertNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 2) - } - - func testWhenSecondTimeOnHomeScreenAndFireEducationSeenThenShowSubsequentDialog() { - XCTAssertFalse(onboarding.shouldShowFireButtonPulse) - XCTAssertNotNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - XCTAssertNotNil(onboarding.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) - XCTAssertTrue(onboarding.shouldShowFireButtonPulse) - XCTAssertEqual(DaxDialogs.HomeScreenSpec.final, onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 2) - } - - func testWhenSecondTimeOnHomeScreenAndNoOtherDialogsSeenThenShowNothing() { - XCTAssertNotNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - XCTAssertNil(onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - } - - func testWhenFirstTimeOnHomeScreenThenShowFirstDialog() { - XCTAssertEqual(DaxDialogs.HomeScreenSpec.initial, onboarding.nextHomeScreenMessage()) - XCTAssertEqual(settings.homeScreenMessagesSeen, 1) - } func testWhenPrimingDaxDialogForUseThenDismissedIsFalse() { let settings = InMemoryDaxDialogsSettings() @@ -304,12 +124,9 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(DefaultDaxDialogsSettings().isDismissed) } - // MARK: - Experiment - - func testWhenExperimentAndBrowsingSpecIsWithOneTrackerThenHighlightAddressBarIsFalse() throws { + func testWhenBrowsingSpecIsWithOneTrackerThenHighlightAddressBarIsFalse() throws { // GIVEN - mockVariantManager.isSupportedReturns = true - let sut = makeExperimentSUT(settings: InMemoryDaxDialogsSettings()) + let sut = makeSUT(settings: InMemoryDaxDialogsSettings()) let privacyInfo = makePrivacyInfo(url: URLs.example) let detectedTracker = detectedTrackerFrom(URLs.google, pageUrl: URLs.example.absoluteString) privacyInfo.trackerInfo.addDetectedTracker(detectedTracker, onPageWithURL: URLs.example) @@ -322,10 +139,9 @@ final class DaxDialog: XCTestCase { XCTAssertFalse(result.highlightAddressBar) } - func testWhenExperimentAndBrowsingSpecIsWithMultipleTrackerThenHighlightAddressBarIsFalse() throws { + func testWhenBrowsingSpecIsWithMultipleTrackerThenHighlightAddressBarIsFalse() throws { // GIVEN - mockVariantManager.isSupportedReturns = true - let sut = makeExperimentSUT(settings: InMemoryDaxDialogsSettings()) + let sut = makeSUT(settings: InMemoryDaxDialogsSettings()) let privacyInfo = makePrivacyInfo(url: URLs.example) [URLs.google, URLs.amazon].forEach { tracker in let detectedTracker = detectedTrackerFrom(tracker, pageUrl: URLs.example.absoluteString) @@ -340,11 +156,11 @@ final class DaxDialog: XCTestCase { XCTAssertFalse(result.highlightAddressBar) } - func testWhenExperimentGroupAndURLIsDuckDuckGoSearchAndSearchDialogHasNotBeenSeenThenReturnSpecTypeAfterSearch() { + func testWhenURLIsDuckDuckGoSearchAndSearchDialogHasNotBeenSeenThenReturnSpecTypeAfterSearch() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingAfterSearchShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg)) @@ -353,11 +169,11 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .afterSearch) } - func testWhenExperimentGroupAndURLIsMajorTrackerWebsiteAndMajorTrackerDialogHasNotBeenSeenThenReturnSpecTypeSiteIsMajorTracker() { + func testWhenURLIsMajorTrackerWebsiteAndMajorTrackerDialogHasNotBeenSeenThenReturnSpecTypeSiteIsMajorTracker() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingMajorTrackingSiteShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.facebook) // WHEN @@ -367,11 +183,11 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .siteIsMajorTracker) } - func testWhenExperimentGroupAndURLIsOwnedByMajorTrackerAndMajorTrackerDialogHasNotBeenSeenThenReturnSpecTypeSiteOwnedByMajorTracker() { + func testWhenURLIsOwnedByMajorTrackerAndMajorTrackerDialogHasNotBeenSeenThenReturnSpecTypeSiteOwnedByMajorTracker() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingMajorTrackingSiteShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.ownedByFacebook) // WHEN @@ -381,11 +197,11 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .siteOwnedByMajorTracker) } - func testWhenExperimentGroupAndURLHasTrackersAndMultipleTrackersDialogHasNotBeenSeenThenReturnSpecTypeWithMultipleTrackers() { + func testWhenURLHasTrackersAndMultipleTrackersDialogHasNotBeenSeenThenReturnSpecTypeWithMultipleTrackers() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.example) [URLs.google, URLs.amazon].forEach { url in let detectedTracker = detectedTrackerFrom(url, pageUrl: URLs.example.absoluteString) @@ -399,11 +215,11 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .withMultipleTrackers) } - func testWhenExperimentGroupAndURLHasNoTrackersAndIsNotSERPAndNoTrakcersDialogHasNotBeenSeenThenReturnSpecTypeWithoutTrackers() { + func testWhenURLHasNoTrackersAndIsNotSERPAndNoTrakcersDialogHasNotBeenSeenThenReturnSpecTypeWithoutTrackers() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithoutTrackersShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example)) @@ -412,13 +228,11 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .withoutTrackers) } - func testWhenExperimentGroupAndURLIsDuckDuckGoSearchAndHasVisitedWebsiteThenSpecTypeSearchIsReturned() throws { + func testWhenURLIsDuckDuckGoSearchAndHasVisitedWebsiteThenSpecTypeSearchIsReturned() throws { try [DaxDialogs.BrowsingSpec.withoutTrackers, .siteIsMajorTracker, .siteOwnedByMajorTracker, .withOneTracker, .withMultipleTrackers].forEach { spec in // GIVEN - let isExperiment = true - let mockVariantManager = MockVariantManager(isSupportedReturns: isExperiment) let settings = InMemoryDaxDialogsSettings() - let sut = DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) + let sut = DaxDialogs(settings: settings, entityProviding: entityProvider) sut.overrideShownFlagFor(spec, flag: true) // WHEN @@ -429,12 +243,12 @@ final class DaxDialog: XCTestCase { } } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogNotSeen_AndSearchDone_ThenFinalBrowsingSpecIsReturned() throws { + func testWhenFireButtonSeen_AndFinalDialogNotSeen_AndSearchDone_ThenFinalBrowsingSpecIsReturned() throws { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingAfterSearchShown = true settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = try XCTUnwrap(sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg))) @@ -443,12 +257,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .final) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogNotSeen_AndWebsiteWithoutTracker_ThenFinalBrowsingSpecIsReturned() throws { + func testWhenFireButtonSeen_AndFinalDialogNotSeen_AndWebsiteWithoutTracker_ThenFinalBrowsingSpecIsReturned() throws { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithoutTrackersShown = true settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = try XCTUnwrap(sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example))) @@ -457,12 +271,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .final) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogNotSeen_AndWebsiteWithTracker_ThenFinalBrowsingSpecIsReturned() throws { + func testWhenFireButtonSeen_AndFinalDialogNotSeen_AndWebsiteWithTracker_ThenFinalBrowsingSpecIsReturned() throws { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.example) let detectedTracker = detectedTrackerFrom(URLs.google, pageUrl: URLs.example.absoluteString) privacyInfo.trackerInfo.addDetectedTracker(detectedTracker, onPageWithURL: URLs.example) @@ -474,12 +288,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .final) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogNotSeen_AndWebsiteMajorTracker_ThenFinalBrowsingSpecIsReturned() throws { + func testWhenFireButtonSeen_AndFinalDialogNotSeen_AndWebsiteMajorTracker_ThenFinalBrowsingSpecIsReturned() throws { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingMajorTrackingSiteShown = true settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.ownedByFacebook) // WHEN @@ -489,13 +303,13 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .final) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogSeen_AndSearchDone_ThenBrowsingSpecIsNil() { + func testWhenFireButtonSeen_AndFinalDialogSeen_AndSearchDone_ThenBrowsingSpecIsNil() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingAfterSearchShown = true settings.fireMessageExperimentShown = true settings.browsingFinalDialogShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg)) @@ -504,13 +318,13 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogSeen_AndWebsiteWithoutTracker_ThenBrowsingSpecIsNotFinal() { + func testWhenFireButtonSeen_AndFinalDialogSeen_AndWebsiteWithoutTracker_ThenBrowsingSpecIsNotFinal() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithoutTrackersShown = true settings.fireMessageExperimentShown = true settings.browsingFinalDialogShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example)) @@ -519,13 +333,13 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogSeen_AndWebsiteWithTracker_ThenBrowsingSpecIsNil() { + func testWhenFireButtonSeen_AndFinalDialogSeen_AndWebsiteWithTracker_ThenBrowsingSpecIsNil() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.fireMessageExperimentShown = true settings.browsingFinalDialogShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.example) let detectedTracker = detectedTrackerFrom(URLs.google, pageUrl: URLs.example.absoluteString) privacyInfo.trackerInfo.addDetectedTracker(detectedTracker, onPageWithURL: URLs.example) @@ -537,13 +351,13 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogSeen_AndWebsiteMajorTracker_ThenFinalBrowsingSpecIsReturned() { + func testWhenFireButtonSeen_AndFinalDialogSeen_AndWebsiteMajorTracker_ThenFinalBrowsingSpecIsReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingMajorTrackingSiteShown = true settings.fireMessageExperimentShown = true settings.browsingFinalDialogShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) let privacyInfo = makePrivacyInfo(url: URLs.ownedByFacebook) // WHEN @@ -553,7 +367,7 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } - func testWhenExperimentGroup_AndFireButtonSeen_AndFinalDialogSeen_AndSearchNotSeen_ThenAfterSearchSpecIsReturned() { + func testWhenFireButtonSeen_AndFinalDialogSeen_AndSearchNotSeen_ThenAfterSearchSpecIsReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithoutTrackersShown = true @@ -561,7 +375,7 @@ final class DaxDialog: XCTestCase { settings.browsingMajorTrackingSiteShown = true settings.fireMessageExperimentShown = true settings.browsingFinalDialogShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg)) @@ -570,10 +384,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .afterSearch) } - func testWhenExperimentGroup_AndSearchDialogSeen_OnReload_SearchDialogReturned() { + func testWhenSearchDialogSeen_OnReload_SearchDialogReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg)) @@ -584,10 +398,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result1, result2) } - func testWhenExperimentGroup_AndSearchDialogSeen_OnLoadingAnotherSearch_NilReturned() { + func testWhenSearchDialogSeen_OnLoadingAnotherSearch_NilReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ddg)) @@ -598,10 +412,10 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result2) } - func testWhenExperimentGroup_AndMajorTrackerDialogSeen_OnReload_MajorTrackerDialogReturned() { + func testWhenMajorTrackerDialogSeen_OnReload_MajorTrackerDialogReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.facebook)) @@ -612,10 +426,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result1, result2) } - func testWhenExperimentGroup_AndMajorTrackerDialogSeen_OnLoadingAnotherSearch_NilReturned() { + func testWhenMajorTrackerDialogSeen_OnLoadingAnotherSearch_NilReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.facebook)) @@ -626,10 +440,10 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result2) } - func testWhenExperimentGroup_AndMajorTrackerOwnerMessageSeen_OnReload_MajorTrackerOwnerDialogReturned() { + func testWhenMajorTrackerOwnerMessageSeen_OnReload_MajorTrackerOwnerDialogReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ownedByFacebook)) @@ -640,10 +454,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result1, result2) } - func testWhenExperimentGroup_AndMajorTrackerOwnerMessageSeen_OnLoadingAnotherSearch_NilReturned() { + func testWhenMajorTrackerOwnerMessageSeen_OnLoadingAnotherSearch_NilReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ownedByFacebook)) @@ -654,10 +468,10 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result2) } - func testWhenExperimentGroup_AndWithoutTrackersMessageSeen_OnReload_WithoutTrackersDialogReturned() { + func testWhenWithoutTrackersMessageSeen_OnReload_WithoutTrackersDialogReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.tracker)) @@ -668,10 +482,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result1, result2) } - func testWhenExperimentGroup_AndWithoutTrackersMessageSeen_OnLoadingAnotherSearch_NilReturned() { + func testWhenWithoutTrackersMessageSeen_OnLoadingAnotherSearch_NilReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.tracker)) @@ -682,12 +496,12 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result2) } - func testWhenExperimentGroup_AndFinalMessageSeen_OnReload_NilReturned() { + func testWhenFinalMessageSeen_OnReload_NilReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithoutTrackersShown = true settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result1 = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.example)) @@ -698,10 +512,10 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result2) } - func testWhenExperimentGroup_AndVisitWebsiteSeen_OnReload_VisitWebsiteReturned() { + func testWhenVisitWebsiteSeen_OnReload_VisitWebsiteReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.setSearchMessageSeen() // WHEN @@ -716,10 +530,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result2, result3) } - func testWhenExperimentGroup_AndVisitWebsiteSeen_OnLoadingAnotherSearch_NilIseturned() { + func testWhenVisitWebsiteSeen_OnLoadingAnotherSearch_NilIseturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.setSearchMessageSeen() // WHEN @@ -734,10 +548,10 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result3) } - func testWhenExperimentGroup_AndFireMessageSeen_OnReload_FireMessageReturned() { + func testWhenFireMessageSeen_OnReload_FireMessageReturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.setSearchMessageSeen() // WHEN @@ -752,10 +566,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result2, result3) } - func testWhenExperimentGroup_AndSearchNotSeen_AndFireMessageSeen_OnLoadingAnotherSearch_ExpectedDialogIseturned() { + func testWhenSearchNotSeen_AndFireMessageSeen_OnLoadingAnotherSearch_ExpectedDialogIseturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.setSearchMessageSeen() // WHEN @@ -770,10 +584,10 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result3?.type, .afterSearch) } - func testWhenExperimentGroup_AndSearchSeen_AndFireMessageSeen_OnLoadingAnotherSearch_ExpectedDialogIseturned() { + func testWhenSearchSeen_AndFireMessageSeen_OnLoadingAnotherSearch_ExpectedDialogIseturned() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.setSearchMessageSeen() // WHEN @@ -791,12 +605,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result4?.type, .final) } - func testWhenExperimentGroup_AndBrowserWithTrackersShown_AndPrivacyAnimationNotShown_ThenShowPrivacyAnimationPulse() { + func testWhenBrowserWithTrackersShown_AndPrivacyAnimationNotShown_ThenShowPrivacyAnimationPulse() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.privacyButtonPulseShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.shouldShowPrivacyButtonPulse @@ -805,12 +619,12 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(result) } - func testWhenExperimentGroup_AndBrowserWithTrackersShown_AndPrivacyAnimationShown_ThenDoNotShowPrivacyAnimationPulse() { + func testWhenBrowserWithTrackersShown_AndPrivacyAnimationShown_ThenDoNotShowPrivacyAnimationPulse() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.privacyButtonPulseShown = true - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.shouldShowPrivacyButtonPulse @@ -819,12 +633,12 @@ final class DaxDialog: XCTestCase { XCTAssertFalse(result) } - func testWhenExperimentGroup_AndBrowserWithTrackersShown_AndFireButtonPulseActive_ThenDoNotShowPrivacyAnimationPulse() { + func testWhenBrowserWithTrackersShown_AndFireButtonPulseActive_ThenDoNotShowPrivacyAnimationPulse() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.privacyButtonPulseShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.fireButtonPulseStarted() // WHEN @@ -834,10 +648,10 @@ final class DaxDialog: XCTestCase { XCTAssertFalse(result) } - func testWhenExperimentGroup_AndCallSetPrivacyButtonPulseSeen_ThenSetPrivacyButtonPulseShownFlagToTrue() { + func testWhenCallSetPrivacyButtonPulseSeen_ThenSetPrivacyButtonPulseShownFlagToTrue() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) XCTAssertFalse(settings.privacyButtonPulseShown) // WHEN @@ -847,10 +661,10 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(settings.privacyButtonPulseShown) } - func testWhenExperimentGroup_AndSetFireEducationMessageSeenIsCalled_ThenSetPrivacyButtonPulseShownToTrue() { + func testWhenSetFireEducationMessageSeenIsCalled_ThenSetPrivacyButtonPulseShownToTrue() { // GIVEN let settings = InMemoryDaxDialogsSettings() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) XCTAssertFalse(settings.privacyButtonPulseShown) // WHEN @@ -860,13 +674,13 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(settings.privacyButtonPulseShown) } - func testWhenExperimentGroup_AndFireButtonAnimationPulseNotShown__AndShouldShowFireButtonPulseIsCalled_ThenReturnTrue() { + func testWhenFireButtonAnimationPulseNotShown__AndShouldShowFireButtonPulseIsCalled_ThenReturnTrue() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.privacyButtonPulseShown = true settings.browsingWithTrackersShown = true settings.fireButtonPulseDateShown = nil - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.shouldShowFireButtonPulse @@ -875,13 +689,13 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(result) } - func testWhenExperimentGroup_AndFireButtonAnimationPulseShown_AndShouldShowFireButtonPulseIsCalled_ThenReturnFalse() { + func testWhenFireButtonAnimationPulseShown_AndShouldShowFireButtonPulseIsCalled_ThenReturnFalse() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.privacyButtonPulseShown = true settings.browsingWithTrackersShown = true settings.fireButtonPulseDateShown = Date() - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.shouldShowFireButtonPulse @@ -890,12 +704,12 @@ final class DaxDialog: XCTestCase { XCTAssertFalse(result) } - func testWhenExperimentGroup_AndFireEducationMessageSeen_AndFinalMessageNotSeen_ThenShowFinalMessage() { + func testWhenFireEducationMessageSeen_AndFinalMessageNotSeen_ThenShowFinalMessage() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.fireMessageExperimentShown = true settings.browsingFinalDialogShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextHomeScreenMessageNew() @@ -904,12 +718,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .final) } - func testWhenExperimentGroup_AndNextHomeScreenMessageNewIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { + func testWhenNextHomeScreenMessageNewIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) @@ -921,54 +735,24 @@ final class DaxDialog: XCTestCase { XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) } - func testWhenExperimentGroup_AndCanEnableAddFavoritesFlowIsCalled_ThenReturnFalse() { + func testWhenEnableAddFavoritesFlowIsCalled_ThenIsAddFavoriteFlowIsTrue() { // GIVEN - let sut = makeExperimentSUT(settings: InMemoryDaxDialogsSettings()) - - // WHEN - let result = sut.canEnableAddFavoriteFlow() - - // THEN - XCTAssertFalse(result) - } - - func testWhenControlGroup_AndCanEnableAddFavoritesFlowIsCalled_ThenReturnTrue() { - // WHEN - let result = onboarding.canEnableAddFavoriteFlow() - - // THEN - XCTAssertTrue(result) - } - - func testWhenControlGroup_AndEnableAddFavoritesFlowIsCalled_ThenIsAddFavoriteFlowIsTrue() { - // GIVEN - XCTAssertFalse(onboarding.isAddFavoriteFlow) - - // WHEN - onboarding.enableAddFavoriteFlow() - - // THEN - XCTAssertTrue(onboarding.isAddFavoriteFlow) - } - - func testWhenExperimentGroup_AndEnableAddFavoritesFlowIsCalled_ThenIsAddFavoriteFlowIsFalse() { - // GIVEN - let sut = makeExperimentSUT(settings: InMemoryDaxDialogsSettings()) + let sut = makeSUT(settings: InMemoryDaxDialogsSettings()) XCTAssertFalse(sut.isAddFavoriteFlow) // WHEN sut.enableAddFavoriteFlow() // THEN - XCTAssertFalse(sut.isAddFavoriteFlow) + XCTAssertTrue(sut.isAddFavoriteFlow) } - func testWhenExperimentGroup_AndBlockedTrackersDialogSeen_AndMajorTrackerNotSeen_ThenReturnNilSpec() { + func testWhenBlockedTrackersDialogSeen_AndMajorTrackerNotSeen_ThenReturnNilSpec() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.browsingMajorTrackingSiteShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.facebook)) @@ -977,12 +761,12 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } - func testWhenExperimentGroup_AndBlockedTrackersDialogNotSeen_AndMajorTrackerNotSeen_ThenReturnMajorNetworkSpec() { + func testWhenBlockedTrackersDialogNotSeen_AndMajorTrackerNotSeen_ThenReturnMajorNetworkSpec() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = false settings.browsingMajorTrackingSiteShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.facebook)) @@ -991,12 +775,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .siteIsMajorTracker) } - func testWhenExperimentGroup_AndBlockedTrackersDialogSeen_AndOwnedByMajorTrackerNotSeen_ThenReturnNilSpec() { + func testWhenBlockedTrackersDialogSeen_AndOwnedByMajorTrackerNotSeen_ThenReturnNilSpec() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = true settings.browsingMajorTrackingSiteShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ownedByFacebook)) @@ -1005,12 +789,12 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } - func testWhenExperimentGroup_AndBlockedTrackersDialogNotSeen_AndOwnedByMajorTrackerNotSeen_ThenReturnOwnedByMajorNetworkSpec() { + func testWhenBlockedTrackersDialogNotSeen_AndOwnedByMajorTrackerNotSeen_ThenReturnOwnedByMajorNetworkSpec() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.browsingWithTrackersShown = false settings.browsingMajorTrackingSiteShown = false - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) // WHEN let result = sut.nextBrowsingMessageIfShouldShow(for: makePrivacyInfo(url: URLs.ownedByFacebook)) @@ -1019,12 +803,12 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result?.type, .siteOwnedByMajorTracker) } - func testWhenExperimentGroup_AndDismissIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { + func testWhenDismissIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) @@ -1036,12 +820,12 @@ final class DaxDialog: XCTestCase { XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) } - func testWhenExperimentGroup_AndSetDaxDialogDismiss_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { + func testWhenSetDaxDialogDismiss_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) @@ -1053,12 +837,12 @@ final class DaxDialog: XCTestCase { XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) } - func testWhenExperimentGroup_AndClearedBrowserDataIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() throws { + func testWhenClearedBrowserDataIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() throws { // GIVEN let settings = InMemoryDaxDialogsSettings() settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) @@ -1070,14 +854,14 @@ final class DaxDialog: XCTestCase { XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) } - func testWhenExperimentGroup_AndIsEnabledIsFalse_AndReloadWebsite_ThenReturnNilBrowsingSpec() throws { + func testWhenIsEnabledIsFalse_AndReloadWebsite_ThenReturnNilBrowsingSpec() throws { // GIVEN let lastVisitedWebsitePath = "https://www.example.com" let lastVisitedWebsiteURL = try XCTUnwrap(URL(string: lastVisitedWebsitePath)) let settings = InMemoryDaxDialogsSettings() settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue settings.lastVisitedOnboardingWebsiteURLPath = lastVisitedWebsitePath - let sut = makeExperimentSUT(settings: settings) + let sut = makeSUT(settings: settings) sut.dismiss() // WHEN @@ -1089,9 +873,7 @@ final class DaxDialog: XCTestCase { func testWhenIsEnabledIsCalled_AndShouldShowDaxDialogsIsTrue_ThenReturnTrue() { // GIVEN - var mockVariantManager = MockVariantManager() - mockVariantManager.currentVariant = MockVariant(features: [.newOnboardingIntro, .contextualDaxDialogs]) - let sut = DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) + let sut = DaxDialogs(settings: settings, entityProviding: entityProvider) // WHEN let result = sut.isEnabled @@ -1107,7 +889,7 @@ final class DaxDialog: XCTestCase { let onboardingManagerMock = OnboardingManagerMock() onboardingManagerMock.addToDockEnabledState = .contextual settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + let sut = makeSUT(settings: settings, onboardingManager: onboardingManagerMock) _ = sut.nextHomeScreenMessageNew() // WHEN @@ -1121,7 +903,7 @@ final class DaxDialog: XCTestCase { // GIVEN let onboardingManagerMock = OnboardingManagerMock() onboardingManagerMock.addToDockEnabledState = .contextual - let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + let sut = makeSUT(settings: settings, onboardingManager: onboardingManagerMock) _ = sut.nextHomeScreenMessageNew() // WHEN @@ -1136,7 +918,7 @@ final class DaxDialog: XCTestCase { let onboardingManagerMock = OnboardingManagerMock() onboardingManagerMock.addToDockEnabledState = .disabled settings.fireMessageExperimentShown = true - let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + let sut = makeSUT(settings: settings, onboardingManager: onboardingManagerMock) _ = sut.nextHomeScreenMessageNew() // WHEN @@ -1169,11 +951,7 @@ final class DaxDialog: XCTestCase { protectionStatus: protectionStatus) } - private func makeExperimentSUT(settings: DaxDialogsSettings, onboardingManager: OnboardingAddToDockManaging = OnboardingManagerMock()) -> DaxDialogs { - var mockVariantManager = MockVariantManager() - mockVariantManager.isSupportedBlock = { feature in - feature == .contextualDaxDialogs - } - return DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager, onboardingManager: onboardingManager) + private func makeSUT(settings: DaxDialogsSettings, onboardingManager: OnboardingAddToDockManaging = OnboardingManagerMock()) -> DaxDialogs { + DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: MockVariantManager(), onboardingManager: onboardingManager) } } diff --git a/DuckDuckGoTests/DaxDialogsNewTabTests.swift b/DuckDuckGoTests/DaxDialogsNewTabTests.swift index 23f3617765..d99af3291e 100644 --- a/DuckDuckGoTests/DaxDialogsNewTabTests.swift +++ b/DuckDuckGoTests/DaxDialogsNewTabTests.swift @@ -38,7 +38,6 @@ final class DaxDialogsNewTabTests: XCTestCase { } func testIfIsAddFavoriteFlow_OnNextHomeScreenMessageNew_ReturnsAddFavorite() { - XCTExpectFailure("Add Favrite flow, is currenty disabled for new onboarding. Remove failure expectation once we support it.") // GIVEN daxDialogs.enableAddFavoriteFlow() diff --git a/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift b/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift deleted file mode 100644 index f9641c8fd1..0000000000 --- a/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// DefaultVariantManagerOnboardingTests.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 BrowserServicesKit -@testable import Core -@testable import DuckDuckGo - -final class DefaultVariantManagerOnboardingTests: XCTestCase { - - // MARK: - Is Onboarding Highlights - - func testWhenIsOnboardingHighlights_AndFeaturesContainOnboardingHighlights_ThenReturnTrue() { - // GIVEN - let sut = makeVariantManager(features: [.newOnboardingIntroHighlights]) - - // WHEN - let result = sut.isOnboardingHighlightsExperiment - - // THEN - XCTAssertTrue(result) - } - - func testWhenIsOnboardingHighlights_AndFeaturesDoNotContainOnboardingHighlights_ThenReturnFalse() { - // GIVEN - let sut = makeVariantManager(features: [.newOnboardingIntro, .contextualDaxDialogs]) - - // WHEN - let result = sut.isOnboardingHighlightsExperiment - - // THEN - XCTAssertFalse(result) - } - - func testWhenIsOnboardingHighlights_AndFeaturesIsEmpty_ThenReturnFalse() { - // GIVEN - let sut = makeVariantManager(features: []) - - // WHEN - let result = sut.isOnboardingHighlightsExperiment - - // THEN - XCTAssertFalse(result) - } - - // MARK: - Is Contextual Dax Dialogs Enabled - - func testWhenIsContextualDaxDialogsEnabled_AndFeaturesContainContextualDaxDialogs_ThenReturnTrue() { - // GIVEN - let sut = makeVariantManager(features: [.contextualDaxDialogs]) - - // WHEN - let result = sut.isContextualDaxDialogsEnabled - - // THEN - XCTAssertTrue(result) - } - - func testWhenIsContextualDaxDialogsEnabled_AndFeaturesDoNotContainContextualDaxDialogs_ThenReturnFalse() { - // GIVEN - let sut = makeVariantManager(features: [.newOnboardingIntro, .newOnboardingIntroHighlights]) - - // WHEN - let result = sut.isContextualDaxDialogsEnabled - - // THEN - XCTAssertFalse(result) - } - - func testWhenIsContextualDaxDialogsEnabled_AndFeaturesIsEmpty_ThenReturnFalse() { - // GIVEN - let sut = makeVariantManager(features: []) - - // WHEN - let result = sut.isContextualDaxDialogsEnabled - - // THEN - XCTAssertFalse(result) - } - -} - -// MARK: Helpers - -private extension DefaultVariantManagerOnboardingTests { - - func makeVariantManager(features: [FeatureName]) -> DefaultVariantManager { - let mockStatisticStore = MockStatisticsStore() - mockStatisticStore.variant = #function - let variantManager = DefaultVariantManager( - variants: [VariantIOS(name: #function, weight: 1, isIncluded: VariantIOS.When.always, features: features)], - storage: mockStatisticStore, - rng: MockVariantRNG(returnValue: 500), - returningUserMeasurement: MockReturningUserMeasurement(), - variantNameOverride: MockVariantNameOverride() - ) - variantManager.assignVariantIfNeeded { _ in } - return variantManager - } - -} diff --git a/DuckDuckGoTests/HomeRowReminderTests.swift b/DuckDuckGoTests/HomeRowReminderTests.swift index c0e44358f5..33c310d1e7 100644 --- a/DuckDuckGoTests/HomeRowReminderTests.swift +++ b/DuckDuckGoTests/HomeRowReminderTests.swift @@ -60,6 +60,40 @@ class HomeRowReminderTests: XCTestCase { XCTAssertFalse(feature.showNow()) } + // MARK: - Add To Dock - Onboarding + + func testWhenAddToDockHasShownInOboardingIntroThenDoNotShowAddToDockReminder() { + // GIVEN + var variantManager = MockVariantManager() + variantManager.isSupportedBlock = { feature in + feature == .addToDockIntro + } + let sut = HomeRowReminder(storage: storage, variantManager: variantManager) + + // WHEN + let result = sut.showNow() + + // THEN + XCTAssertFalse(result) + } + + func testWhenAddToDockHasShownInContextualOboardingThenDoNotShowAddToDockReminder() { + // GIVEN + var variantManager = MockVariantManager() + variantManager.isSupportedBlock = { feature in + feature == .addToDockContextual + } + let sut = HomeRowReminder(storage: storage, variantManager: variantManager) + + // WHEN + let result = sut.showNow() + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Helper functions + private func setReminderTimeElapsed() { let threeAndABitDaysAgo = -(60 * 60 * 24 * HomeRowReminder.Constants.reminderTimeInDays * 1.1) storage.firstAccessDate = Date(timeIntervalSinceNow: threeAndABitDaysAgo) @@ -67,6 +101,8 @@ class HomeRowReminderTests: XCTestCase { } +// MARK: - Mocks + class MockHomeRowReminderStorage: HomeRowReminderStorage { var firstAccessDate: Date? diff --git a/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift index 1c6322925e..1928a28e03 100644 --- a/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift @@ -82,9 +82,8 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { hvc = nil } - func testWhenContextualDaxDialogsSupported_OnDidAppear_CorrectTypePassedToDialogFactory() throws { + func testWhenViewDidAppear_CorrectTypePassedToDialogFactory() throws { // GIVEN - variantManager.supportedFeatures = [.contextualDaxDialogs] let expectedSpec = randomDialogType() specProvider.specToReturn = expectedSpec @@ -92,30 +91,14 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { hvc.viewDidAppear(false) // THEN - XCTAssertEqual(self.variantManager.capturedFeatureName?.rawValue, FeatureName.contextualDaxDialogs.rawValue) XCTAssertFalse(self.specProvider.nextHomeScreenMessageCalled) XCTAssertTrue(self.specProvider.nextHomeScreenMessageNewCalled) XCTAssertEqual(self.dialogFactory.homeDialog, expectedSpec) XCTAssertNotNil(self.dialogFactory.onDismiss) } - func testWhenOldOnboarding_OnDidAppear_NothingPassedDialogFactory() throws { + func testWhenOnboardingComplete_CorrectTypePassedToDialogFactory() throws { // GIVEN - variantManager.supportedFeatures = [] - - // WHEN - hvc.viewDidAppear(false) - - // THEN - XCTAssertTrue(specProvider.nextHomeScreenMessageCalled) - XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) - XCTAssertNil(dialogFactory.homeDialog) - XCTAssertNil(dialogFactory.onDismiss) - } - - func testWhenContextualDaxDialogsSupported_OnOnboardingComplete_CorrectTypePassedToDialogFactory() throws { - // GIVEN - variantManager.supportedFeatures = [.contextualDaxDialogs] let expectedSpec = randomDialogType() specProvider.specToReturn = expectedSpec @@ -123,51 +106,18 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { hvc.onboardingCompleted() // THEN - XCTAssertEqual(self.variantManager.capturedFeatureName?.rawValue, FeatureName.contextualDaxDialogs.rawValue) XCTAssertFalse(self.specProvider.nextHomeScreenMessageCalled) XCTAssertTrue(self.specProvider.nextHomeScreenMessageNewCalled) XCTAssertEqual(self.dialogFactory.homeDialog, expectedSpec) XCTAssertNotNil(self.dialogFactory.onDismiss) } - func testWhenOldOnboarding_OnOnboardingComplete_NothingPassedDialogFactory() throws { - // GIVEN - variantManager.supportedFeatures = [] - - // WHEN - hvc.onboardingCompleted() - - // THEN - XCTAssertTrue(specProvider.nextHomeScreenMessageCalled) - XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) - XCTAssertNil(dialogFactory.homeDialog) - XCTAssertNil(dialogFactory.onDismiss) - } - - func testWhenOldOnboarding_OnOpenedAsNewTab_NothingPassedDialogFactory() throws { - // GIVEN - variantManager.supportedFeatures = [] - - // WHEN - hvc.openedAsNewTab(allowingKeyboard: true) - - // THEN - XCTAssertTrue(specProvider.nextHomeScreenMessageCalled) - XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) - XCTAssertNil(dialogFactory.homeDialog) - XCTAssertNil(dialogFactory.onDismiss) - } - func testWhenShowNextDaxDialog_AndShouldShowDaxDialogs_ThenReturnTrue() { - // GIVEN - variantManager.supportedFeatures = [] - // WHEN hvc.showNextDaxDialog() // THEN - XCTAssertTrue(specProvider.nextHomeScreenMessageCalled) - XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) + XCTAssertTrue(specProvider.nextHomeScreenMessageNewCalled) } private func randomDialogType() -> DaxDialogs.HomeScreenSpec { diff --git a/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift b/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift index 5708195ae5..f11b62c610 100644 --- a/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift @@ -44,15 +44,15 @@ final class OnboardingAddressBarPositionPickerViewModelTests: XCTestCase { // THEN let firstItem = try XCTUnwrap(items.first) XCTAssertEqual(firstItem.type, .top) - XCTAssertEqual(firstItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle + " " + UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) - XCTAssertEqual(firstItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage) + XCTAssertEqual(firstItem.title.string, UserText.Onboarding.AddressBarPosition.topTitle + " " + UserText.Onboarding.AddressBarPosition.defaultOption) + XCTAssertEqual(firstItem.message, UserText.Onboarding.AddressBarPosition.topMessage) XCTAssertEqual(firstItem.icon, .addressBarTop) XCTAssertTrue(firstItem.isSelected) let secondItem = try XCTUnwrap(items.last) XCTAssertEqual(secondItem.type, .bottom) - XCTAssertEqual(secondItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) - XCTAssertEqual(secondItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage) + XCTAssertEqual(secondItem.title.string, UserText.Onboarding.AddressBarPosition.bottomTitle) + XCTAssertEqual(secondItem.message, UserText.Onboarding.AddressBarPosition.bottomMessage) XCTAssertEqual(secondItem.icon, .addressBarBottom) XCTAssertFalse(secondItem.isSelected) } @@ -73,15 +73,15 @@ final class OnboardingAddressBarPositionPickerViewModelTests: XCTestCase { let items = sut.items let firstItem = try XCTUnwrap(items.first) XCTAssertEqual(firstItem.type, .top) - XCTAssertEqual(firstItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle + " " + UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) - XCTAssertEqual(firstItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage) + XCTAssertEqual(firstItem.title.string, UserText.Onboarding.AddressBarPosition.topTitle + " " + UserText.Onboarding.AddressBarPosition.defaultOption) + XCTAssertEqual(firstItem.message, UserText.Onboarding.AddressBarPosition.topMessage) XCTAssertEqual(firstItem.icon, .addressBarTop) XCTAssertFalse(firstItem.isSelected) let secondItem = try XCTUnwrap(items.last) XCTAssertEqual(secondItem.type, .bottom) - XCTAssertEqual(secondItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) - XCTAssertEqual(secondItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage) + XCTAssertEqual(secondItem.title.string, UserText.Onboarding.AddressBarPosition.bottomTitle) + XCTAssertEqual(secondItem.message, UserText.Onboarding.AddressBarPosition.bottomMessage) XCTAssertEqual(secondItem.icon, .addressBarBottom) XCTAssertTrue(secondItem.isSelected) } diff --git a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift index 3cbcc0c9e7..f7f47e4602 100644 --- a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift +++ b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift @@ -134,18 +134,7 @@ final class OnboardingDaxFavouritesTests: XCTestCase { XCTAssertTrue(result) } - func testWhenAddFavouriteIsCalled_ThenItShouldAskContextualOnboardingLogicIfAddFavoriteFlowCanStart() { - // GIVEN - XCTAssertFalse(contextualOnboardingLogicMock.didCallCanEnableAddFavoriteFlow) - - // WHEN - sut.startAddFavoriteFlow() - - // THEN - XCTAssertTrue(contextualOnboardingLogicMock.didCallCanEnableAddFavoriteFlow) - } - - func testWhenAddFavouriteIsCalled_AndCanStartAddFavouriteFlow_ThenItShouldEnableAddFavouriteFlowOnContextualOnboardingLogic() { + func testWhenAddFavouriteIsCalled_ThenItShouldEnableAddFavouriteFlowOnContextualOnboardingLogic() { // GIVEN contextualOnboardingLogicMock.canStartFavoriteFlow = true XCTAssertFalse(contextualOnboardingLogicMock.didCallEnableAddFavoriteFlow) @@ -157,27 +146,4 @@ final class OnboardingDaxFavouritesTests: XCTestCase { XCTAssertTrue(contextualOnboardingLogicMock.didCallEnableAddFavoriteFlow) } - func testWhenAddFavouriteIsCalled_AndCannotStartAddFavouriteFlow_ThenItShouldNotEnableAddFavouriteFlowOnContextualOnboardingLogic() { - // GIVEN - contextualOnboardingLogicMock.canStartFavoriteFlow = false - XCTAssertFalse(contextualOnboardingLogicMock.didCallEnableAddFavoriteFlow) - - // WHEN - sut.startAddFavoriteFlow() - - // THEN - XCTAssertFalse(contextualOnboardingLogicMock.didCallEnableAddFavoriteFlow) - } - - func testWhenAddFavouriteIsCalled_AndCannotStartAddFavouriteFlow_ThenOpenANewTab() { - // GIVEN - contextualOnboardingLogicMock.canStartFavoriteFlow = false - XCTAssertEqual(sut.tabManager.model.tabs.count, 1) - - // WHEN - sut.startAddFavoriteFlow() - - // THEN - XCTAssertEqual(sut.tabManager.model.tabs.count, 2) - } } diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 8d4f854401..37c3654281 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -58,18 +58,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } - func testWhenStartOnboardingActionIsCalledThenViewStateChangesToBrowsersComparisonDialog() { - // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager) - XCTAssertEqual(sut.state, .landing) - - // WHEN - sut.startOnboardingAction() - - // THEN - XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .hidden))) - } - func testWhenSetDefaultBrowserActionIsCalledThenURLOpenerOpensSettingsURL() { // GIVEN let urlOpenerMock = MockURLOpener() @@ -85,45 +73,10 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(urlOpenerMock.capturedURL?.absoluteString, UIApplication.openSettingsURLString) } - func testWhenSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { - // GIVEN - var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) - sut.onCompletingOnboardingIntro = { - didCallOnCompletingOnboardingIntro = true - } - XCTAssertFalse(didCallOnCompletingOnboardingIntro) - - // WHEN - sut.setDefaultBrowserAction() - - // THEN - XCTAssertTrue(didCallOnCompletingOnboardingIntro) - } - - func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { - // GIVEN - var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) - sut.onCompletingOnboardingIntro = { - didCallOnCompletingOnboardingIntro = true - } - XCTAssertFalse(didCallOnCompletingOnboardingIntro) - - // WHEN - sut.cancelSetDefaultBrowserAction() - - // THEN - XCTAssertTrue(didCallOnCompletingOnboardingIntro) - } - - // MARK: - Highlights State + Actions iPhone + // MARK: iPhone Flow - // MARK: iPhone - - func testWhenSubscribeToViewStateAndIsHighlightsIphoneFlowThenShouldSendLanding() { + func testWhenSubscribeToViewStateAndIsIphoneFlowThenShouldSendLanding() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) // WHEN @@ -133,9 +86,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(result, .landing) } - func testWhenOnAppearIsCalledAndAndIsHighlightsIphoneFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + func testWhenOnAppearIsCalledAndAndIsIphoneFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -146,22 +98,9 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } - func testWhenStartOnboardingActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) - XCTAssertEqual(sut.state, .landing) - // WHEN - sut.startOnboardingAction() - - // THEN - XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 3)))) - } - - func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + func testWhenSetDefaultBrowserActionIsCalledAndIsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -172,9 +111,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) } - func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -185,9 +123,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) } - func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { + func testWhenAppIconPickerContinueActionIsCalledAndIsIphoneFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) XCTAssertEqual(sut.state, .landing) @@ -198,9 +135,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAddressBarPositionDialog, step: .init(currentStep: 3, totalSteps: 3)))) } - func testWhenSelectAddressBarPositionActionIsCalledAndIsHighlightsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { + func testWhenSelectAddressBarPositionActionIsCalledAndIsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true var didCallOnCompletingOnboardingIntro = false let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { @@ -217,9 +153,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { // MARK: iPad - func testWhenSubscribeToViewStateAndIsHighlightsIpadFlowThenShouldSendLanding() { + func testWhenSubscribeToViewStateAndIsIpadFlowThenShouldSendLanding() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) // WHEN @@ -229,9 +164,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(result, .landing) } - func testWhenOnAppearIsCalledAndAndIsHighlightsIpadFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + func testWhenOnAppearIsCalledAndAndIsIpadFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -242,9 +176,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } - func testWhenStartOnboardingActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { + func testWhenStartOnboardingActionIsCalledAndIsIpadFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true) XCTAssertEqual(sut.state, .landing) @@ -255,9 +188,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 2)))) } - func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + func testWhenSetDefaultBrowserActionIsCalledAndIsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -268,9 +200,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 2)))) } - func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -281,9 +212,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 2)))) } - func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { + func testWhenAppIconPickerContinueActionIsCalledAndIsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true var didCallOnCompletingOnboardingIntro = false let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { @@ -339,11 +269,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackChooseBrowserCTAAction) } - // MARK: - Pixel "Highlights" - func testWhenStateChangesToChooseAppIconThenPixelReporterTrackAppIconImpression() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackBrowserComparisonImpression) @@ -357,7 +284,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenAppIconPickerContinueActionIsCalledAndIconIsCustomColorThenPixelReporterTrackCustomAppIconColor() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), appIconProvider: { .purple }) XCTAssertFalse(pixelReporterMock.didCallTrackChooseCustomAppIconColor) @@ -371,7 +297,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenAppIconPickerContinueActionIsCalledAndIconIsDefaultColorThenPixelReporterDoNotTrackCustomAppIconColor() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), appIconProvider: { .defaultAppIcon }) XCTAssertFalse(pixelReporterMock.didCallTrackChooseCustomAppIconColor) @@ -385,7 +310,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStateChangesToChooseAddressBarPositionThenPixelReporterTrackAddressBarSelectionImpression() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackAddressBarPositionSelectionImpression) @@ -399,7 +323,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSelectAddressBarPositionActionIsCalledAndAddressBarPositionIsBottomThenPixelReporterTrackChooseBottomAddressBarPosition() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), addressBarPositionProvider: { .bottom }) XCTAssertFalse(pixelReporterMock.didCallTrackChooseBottomAddressBarPosition) @@ -413,7 +336,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSelectAddressBarPositionActionIsCalledAndAddressBarPositionIsTopThenPixelReporterDoNotTrackChooseBottomAddressBarPosition() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), addressBarPositionProvider: { .top }) XCTAssertFalse(pixelReporterMock.didCallTrackChooseBottomAddressBarPosition) @@ -427,59 +349,32 @@ final class OnboardingIntroViewModelTests: XCTestCase { // MARK: - Copy - func testWhenIsNotHighlightsThenIntroTitleIsCorrect() { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) - - // WHEN - let result = sut.copy.introTitle - - // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.Intro.title) - } - - func testWhenIsHighlightsThenIntroTitleIsCorrect() { + func testIntroTitleIsCorrect() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.copy.introTitle // THEN - XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.Intro.title) - } - - func testWhenIsNotHighlightsThenBrowserComparisonTitleIsCorrect() { - // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) - - // WHEN - let result = sut.copy.browserComparisonTitle - - // THEN - XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.title) + XCTAssertEqual(result, UserText.Onboarding.Intro.title) } - func testWhenIsHighlightsThenBrowserComparisonTitleIsCorrect() { + func testBrowserComparisonTitleIsCorrect() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.copy.browserComparisonTitle // THEN - XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.title) + XCTAssertEqual(result, UserText.Onboarding.BrowsersComparison.title) } // MARK: - Add To Dock - func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToAddToDockPromoDialogAndProgressIs2Of4() { + func testWhenSetDefaultBrowserActionIsCalledAndIsIphoneFlowThenViewStateChangesToAddToDockPromoDialogAndProgressIs2Of4() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true onboardingManager.addToDockEnabledState = .intro let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) @@ -491,9 +386,8 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .addToDockPromoDialog, step: .init(currentStep: 2, totalSteps: 4)))) } - func testWhenAddtoDockContinueActionIsCalledAndIsHighlightsIphoneFlowThenThenViewStateChangesToChooseAppIconAndProgressIs3of4() { + func testWhenAddtoDockContinueActionIsCalledAndIsIphoneFlowThenThenViewStateChangesToChooseAppIconAndProgressIs3of4() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true onboardingManager.addToDockEnabledState = .intro let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) XCTAssertEqual(sut.state, .landing) @@ -509,7 +403,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStateChangesToAddToDockPromoThenPixelReporterTrackAddToDockPromoImpression() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true onboardingManager.addToDockEnabledState = .intro let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) @@ -524,7 +417,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenAddToDockShowTutorialActionIsCalledThenPixelReporterTrackAddToDockPromoShowTutorialCTA() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true onboardingManager.addToDockEnabledState = .intro let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) @@ -539,7 +431,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenAddToDockContinueActionIsCalledAndIsShowingFromAddToDockTutorialIsTrueThenPixelReporterTrackAddToDockTutorialDismissCTA() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true onboardingManager.addToDockEnabledState = .intro let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) @@ -554,7 +445,6 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenAddToDockContinueActionIsCalledAndIsShowingFromAddToDockTutorialIsFalseThenPixelReporterTrackAddToDockTutorialDismissCTA() { // GIVEN - onboardingManager.isOnboardingHighlightsEnabled = true onboardingManager.addToDockEnabledState = .intro let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) diff --git a/DuckDuckGoTests/OnboardingManagerMock.swift b/DuckDuckGoTests/OnboardingManagerMock.swift index dcd8473b60..ad6d3cae1b 100644 --- a/DuckDuckGoTests/OnboardingManagerMock.swift +++ b/DuckDuckGoTests/OnboardingManagerMock.swift @@ -20,7 +20,6 @@ import Foundation @testable import DuckDuckGo -final class OnboardingManagerMock: OnboardingHighlightsManaging, OnboardingAddToDockManaging { - var isOnboardingHighlightsEnabled: Bool = false +final class OnboardingManagerMock: OnboardingAddToDockManaging { var addToDockEnabledState: OnboardingAddToDockState = .disabled } diff --git a/DuckDuckGoTests/OnboardingManagerTests.swift b/DuckDuckGoTests/OnboardingManagerTests.swift index ef7d174fc0..201d577a45 100644 --- a/DuckDuckGoTests/OnboardingManagerTests.swift +++ b/DuckDuckGoTests/OnboardingManagerTests.swift @@ -43,173 +43,105 @@ final class OnboardingManagerTests: XCTestCase { try super.tearDownWithError() } - // MARK: - Onboarding Highlights - - func testWhenIsOnboardingHighlightsLocalFlagEnabledCalledAndAppDefaultsOnboardingHiglightsEnabledIsTrueThenReturnTrue() { - // GIVEN - appSettingsMock.onboardingHighlightsEnabled = true - - // WHEN - let result = sut.isOnboardingHighlightsLocalFlagEnabled - - // THEN - XCTAssertTrue(result) - } + // MARK: - Add to Dock - func testWhenIsOnboardingHighlightsLocalFlagEnabledCalledAndAppDefaultsOnboardingHiglightsEnabledIsFalseThenReturnFalse() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsIntroThenReturnIntro() { // GIVEN - appSettingsMock.onboardingHighlightsEnabled = false + appSettingsMock.onboardingAddToDockState = .intro // WHEN - let result = sut.isOnboardingHighlightsLocalFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .intro) } - func testWhenIsOnboardingHighlightsFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOnThenReturnTrue() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsContextualThenReturnContextual() { // GIVEN - featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] + appSettingsMock.onboardingAddToDockState = .contextual // WHEN - let result = sut.isOnboardingHighlightsFeatureFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertTrue(result) + XCTAssertEqual(result, .contextual) } - func testWhenIsOnboardingHighlightsFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOffThenReturnFalse() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsDisabledThenReturnDisabled() { // GIVEN - featureFlaggerMock.enabledFeatureFlags = [] + appSettingsMock.onboardingAddToDockState = .disabled // WHEN - let result = sut.isOnboardingHighlightsFeatureFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) } - func testWhenIsOnboardingHiglightsEnabledAndIsLocalFlagEnabledIsFalseReturnFalse() { + func testWhenIsAddToDockFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOnThenReturnTrue() { // GIVEN - appSettingsMock.onboardingHighlightsEnabled = false - featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] + featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingAddToDock] // WHEN - let result = sut.isOnboardingHighlightsEnabled + let result = sut.isAddToDockFeatureFlagEnabled // THEN - XCTAssertFalse(result) + XCTAssertTrue(result) } - func testWhenIsOnboardingHiglightsEnabledAndIsFeatureFlagEnabledIsFalseReturnFalse() { + func testWhenIsAddToDockFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOffThenReturnFalse() { // GIVEN - appSettingsMock.onboardingHighlightsEnabled = true featureFlaggerMock.enabledFeatureFlags = [] // WHEN - let result = sut.isOnboardingHighlightsEnabled + let result = sut.isAddToDockFeatureFlagEnabled // THEN XCTAssertFalse(result) } - func testWhenIsOnboardingHiglightsEnabledAndIsLocalFlagEnabledIsTrueAndIsFeatureFlagEnabledIsTrueThenReturnTrue() { + func testWhenAddToDockStateCalledAndVariantManagerSupportsAddToDockIntroThenReturnIntro() { // GIVEN - appSettingsMock.onboardingHighlightsEnabled = true - featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] - - // WHEN - let result = sut.isOnboardingHighlightsEnabled - - // THEN - XCTAssertTrue(result) - } - - func testWhenIsOnboardingHiglightsEnabledAndVariantManagerSupportOnboardingHighlightsReturnTrue() { - // GIVEN - variantManagerMock.isSupportedBlock = { _ in true } - appSettingsMock.onboardingHighlightsEnabled = false - featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] - sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock) - - // WHEN - let result = sut.isOnboardingHighlightsEnabled - - // THEN - XCTAssertTrue(result) - } - - func testWhenIsOnboardingHiglightsEnabledAndVariantManagerSupportOnboardingHighlightsReturnFalse() { - // GIVEN - variantManagerMock.isSupportedBlock = { _ in false } - appSettingsMock.onboardingHighlightsEnabled = false - featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] - sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock) - - // WHEN - let result = sut.isOnboardingHighlightsEnabled - - // THEN - XCTAssertFalse(result) - } + variantManagerMock.isSupportedBlock = { feature in + feature == .addToDockIntro + } + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock, isIphone: true) - // MARK: - Add to Dock - - func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsIntroThenReturnIntro() { - // GIVEN - appSettingsMock.onboardingAddToDockState = .intro // WHEN - let result = sut.addToDockLocalFlagState + let result = sut.addToDockEnabledState // THEN XCTAssertEqual(result, .intro) } - func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsContextualThenReturnContextual() { + func testWhenAddToDockStateCalledAndVariantManagerSupportsAddToDockContextualThenReturnContextual() { // GIVEN - appSettingsMock.onboardingAddToDockState = .contextual + variantManagerMock.isSupportedBlock = { feature in + feature == .addToDockContextual + } + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock, isIphone: true) // WHEN - let result = sut.addToDockLocalFlagState + let result = sut.addToDockEnabledState // THEN XCTAssertEqual(result, .contextual) } - func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsDisabledThenReturnDisabled() { + func testWhenAddToDockStateCalledAndVariantManagerDoesNotSupportAddToDockThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockState = .disabled + variantManagerMock.isSupportedBlock = { _ in + false + } // WHEN - let result = sut.addToDockLocalFlagState + let result = sut.addToDockEnabledState // THEN XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOnThenReturnTrue() { - // GIVEN - featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingAddToDock] - - // WHEN - let result = sut.isAddToDockFeatureFlagEnabled - - // THEN - XCTAssertTrue(result) - } - - func testWhenIsAddToDockFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOffThenReturnFalse() { - // GIVEN - featureFlaggerMock.enabledFeatureFlags = [] - - // WHEN - let result = sut.isAddToDockFeatureFlagEnabled - - // THEN - XCTAssertFalse(result) - } - func testWhenAddToDockStateCalledAndLocalFlagStateIsDisabledAndFeatureFlagIsFalseThenReturnDisabled() { // GIVEN appSettingsMock.onboardingAddToDockState = .disabled diff --git a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift index ced3c4b449..1444ef350a 100644 --- a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift +++ b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift @@ -23,8 +23,7 @@ import Onboarding class OnboardingSuggestedSearchesProviderTests: XCTestCase { private var onboardingManagerMock: OnboardingManagerMock! - let userText = UserText.DaxOnboardingExperiment.ContextualOnboarding.self - let highlightsUserText = UserText.HighlightsOnboardingExperiment.ContextualOnboarding.self + let userText = UserText.Onboarding.ContextualOnboarding.self static let imageSearch = "!image " override func setUpWithError() throws { @@ -39,13 +38,12 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForEnglishLanguageAndUsRegion() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "en") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), - ContextualOnboardingListItem.search(title: userText.tryASearchOption3), - ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeEnglish, visibleTitle: "Surprise me!") + ContextualOnboardingListItem.surprise(title: Self.imageSearch + userText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") ] XCTAssertEqual(provider.list, expectedSearches) @@ -53,13 +51,12 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForNonEnglishLanguageAndNonUSRegion() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "FR", languageCode: "fr") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), ContextualOnboardingListItem.search(title: userText.tryASearchOption2International), - ContextualOnboardingListItem.search(title: userText.tryASearchOption3), - ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeInternational, visibleTitle: "Surprise me!") + ContextualOnboardingListItem.surprise(title: Self.imageSearch + userText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") ] XCTAssertEqual(provider.list, expectedSearches) @@ -67,57 +64,12 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForUSRegionAndNonEnglishLanguage() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "es") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), - ContextualOnboardingListItem.search(title: userText.tryASearchOption3), - ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeEnglish, visibleTitle: "Surprise me!") - ] - - XCTAssertEqual(provider.list, expectedSearches) - } - - // MARK: - Higlights Experiment - - func testWhenHighlightsOnboardingAndSearchesListForEnglishLanguageAndUsRegionThenDoNotReturnOption3() { - onboardingManagerMock.isOnboardingHighlightsEnabled = true - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "en") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) - - let expectedSearches = [ - ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), - ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), - ContextualOnboardingListItem.surprise(title: Self.imageSearch + highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") - ] - - XCTAssertEqual(provider.list, expectedSearches) - } - - func testWhenHighlightsOnboardingAndSearchesListForNonEnglishLanguageAndNonUSRegionThenDoNotReturnOption3() { - onboardingManagerMock.isOnboardingHighlightsEnabled = true - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "FR", languageCode: "fr") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) - - let expectedSearches = [ - ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), - ContextualOnboardingListItem.search(title: userText.tryASearchOption2International), - ContextualOnboardingListItem.surprise(title: Self.imageSearch + highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") - ] - - XCTAssertEqual(provider.list, expectedSearches) - } - - func testWhenHighlightsOnboardingAndSearchesListForUSRegionAndNonEnglishLanguageThenDoNotReturnOption3() { - onboardingManagerMock.isOnboardingHighlightsEnabled = true - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "es") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) - - let expectedSearches = [ - ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), - ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), - ContextualOnboardingListItem.surprise(title: Self.imageSearch + highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ContextualOnboardingListItem.surprise(title: Self.imageSearch + userText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") ] XCTAssertEqual(provider.list, expectedSearches) diff --git a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift index ebb5810a83..ea943dfba3 100644 --- a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift @@ -233,7 +233,6 @@ final class ContextualOnboardingLogicMock: ContextualOnboardingLogic { private(set) var didCallSetFireEducationMessageSeen = false private(set) var didCallsetFinalOnboardingDialogSeen = false private(set) var didCallsetsetSearchMessageSeen = false - private(set) var didCallCanEnableAddFavoriteFlow = false private(set) var didCallEnableAddFavoriteFlow = false private(set) var didCallSetDaxDialogDismiss = false private(set) var didCallClearedBrowserData = false @@ -263,11 +262,6 @@ final class ContextualOnboardingLogicMock: ContextualOnboardingLogic { } - func canEnableAddFavoriteFlow() -> Bool { - didCallCanEnableAddFavoriteFlow = true - return canStartFavoriteFlow - } - func enableAddFavoriteFlow() { didCallEnableAddFavoriteFlow = true } From 5880851a235ddda347258ea42fb421d6953d1e9e Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:22:17 +0100 Subject: [PATCH 2/2] add fake experiment (#3688) Task/Issue URL: https://app.asana.com/0/72649045549333/1208905776162821/f Tech Design URL: CC: **Description**: Used Fake Test feature to generate a cohort and do and A/A test run of the New experiment framework. We want to make sure cohort is assigned and pixels are fired as expected. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/NewTabPageViewModel.swift | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 158ba01632..807e0a8324 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11372,7 +11372,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 218.0.0; + version = 218.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 7d8e4affb7..67a1291f05 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" : "e5d390c8559fbe7b1ca67fd3982c91bcc0437d60", - "version" : "218.0.0" + "revision" : "bbcb41c87c5788718a43883b5b10eb3b4f54aff3", + "version" : "218.0.1" } }, { diff --git a/DuckDuckGo/NewTabPageViewModel.swift b/DuckDuckGo/NewTabPageViewModel.swift index 6cb387b402..5f76ce83b9 100644 --- a/DuckDuckGo/NewTabPageViewModel.swift +++ b/DuckDuckGo/NewTabPageViewModel.swift @@ -19,6 +19,7 @@ import Foundation import Core +import BrowserServicesKit final class NewTabPageViewModel: ObservableObject { @@ -39,6 +40,9 @@ final class NewTabPageViewModel: ObservableObject { isIntroMessageVisible = introDataStorage.newTabPageIntroMessageEnabled ?? false isOnboarding = false isShowingSettings = false + + // This is just temporarily here to run an A/A test to check the new experiment framework works as expected + _ = AppDependencyProvider.shared.featureFlagger.getCohortIfEnabled(for: CredentialsSavingFlag()) } func introMessageDisplayed() { @@ -78,3 +82,19 @@ final class NewTabPageViewModel: ObservableObject { isDragging = false } } + +// This is just temporarily here to run an A/A test to check the new experiment framework works as expected +public struct CredentialsSavingFlag: FeatureFlagExperimentDescribing { + public init() {} + + public typealias CohortType = Cohort + + public var rawValue = "credentialSaving" + + public var source: FeatureFlagSource = .remoteReleasable(.subfeature(ExperimentTestSubfeatures.experimentTestAA)) + + public enum Cohort: String, FlagCohort { + case control + case blue + } +}