diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ed5f16c3c8..f4e1068630 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -722,6 +722,9 @@ 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */; }; 9FDEC7BF2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7BE2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift */; }; 9FDEC7C12C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7C02C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift */; }; + 9FDEC7B62C8FDFD600C7A692 /* OnboardingManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B52C8FDFD600C7A692 /* OnboardingManagerMock.swift */; }; + 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B72C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift */; }; + 9FDEC7BA2C9006E000C7A692 /* BrowserComparisonModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */; }; 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */; }; 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */; }; 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */; }; @@ -2509,6 +2512,9 @@ 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModel.swift; sourceTree = ""; }; 9FDEC7BE2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPicker.swift; sourceTree = ""; }; 9FDEC7C02C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPickerViewModel.swift; sourceTree = ""; }; + 9FDEC7B52C8FDFD600C7A692 /* OnboardingManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManagerMock.swift; sourceTree = ""; }; + 9FDEC7B72C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingIntroViewModel+Copy.swift"; sourceTree = ""; }; + 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserComparisonModelTests.swift; sourceTree = ""; }; 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporter.swift; sourceTree = ""; }; 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterTests.swift; sourceTree = ""; }; 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTextStyles.swift; sourceTree = ""; }; @@ -4674,6 +4680,7 @@ children = ( 9FE08BDB2C2A88FA001D5EBC /* OnboardingIntroViewController.swift */, 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */, + 9FDEC7B72C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift */, 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */, 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */, 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */, @@ -4700,6 +4707,7 @@ 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */, 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */, 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */, + 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */, ); name = Onboarding; sourceTree = ""; @@ -4753,6 +4761,7 @@ 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */, 9F6933182C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift */, 9F6933202C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift */, + 9FDEC7B52C8FDFD600C7A692 /* OnboardingManagerMock.swift */, ); name = Mocks; sourceTree = ""; @@ -7666,6 +7675,7 @@ F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, + 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */, 858566FB252E55D6007501B8 /* ImageCacheDebugViewController.swift in Sources */, D6E0C1832B7A2B1E00D5E1E9 /* DesktopDownloadView.swift in Sources */, 1E7A71172934EB6400B7EA19 /* OmniBarNotificationAnimator.swift in Sources */, @@ -7849,7 +7859,9 @@ 1E1D8B5D2994FFE100C96994 /* AutoconsentMessageProtocolTests.swift in Sources */, 85C11E532090B23A00BFFEB4 /* UserDefaultsHomeRowReminderStorageTests.swift in Sources */, F1DA2F7D1EBCF23700313F51 /* ExternalUrlSchemeTests.swift in Sources */, + 9FDEC7B62C8FDFD600C7A692 /* OnboardingManagerMock.swift in Sources */, F198D78E1E39762C0088DA8A /* StringExtensionTests.swift in Sources */, + 9FDEC7BA2C9006E000C7A692 /* BrowserComparisonModelTests.swift in Sources */, 31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */, D62EC3BC2C2470E000FC9D04 /* DuckPlayerTests.swift in Sources */, 1E8146AE28C8ABF400D1AF63 /* PrivacyIconLogicTests.swift in Sources */, diff --git a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift index 0d60655d97..6f4f855868 100644 --- a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift +++ b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift @@ -117,17 +117,28 @@ 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 case .blockThirdPartyTrackers: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers : UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers case .blockCookiePopups: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups case .blockCreepyAds: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds : UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds case .eraseBrowsingData: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData } } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index 0337ac81ab..ea4362b6f1 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -24,7 +24,7 @@ import DuckUI struct OnboardingTrySearchDialog: View { let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchTitle - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage) + let message: String let viewModel: OnboardingSearchSuggestionsViewModel var body: some View { @@ -33,7 +33,7 @@ struct OnboardingTrySearchDialog: View { ContextualDaxDialogContent( title: title, titleFont: Font(UIFont.daxTitle3()), - message: message, + message: NSAttributedString(string: message), list: viewModel.itemsList, listAction: viewModel.listItemPressed ) @@ -95,8 +95,8 @@ struct OnboardingFireButtonDialogContent: View { } struct OnboardingFirstSearchDoneDialog: View { - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage) let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingGotItButton + let message: String @State private var showNextScreen: Bool = false @@ -112,7 +112,7 @@ struct OnboardingFirstSearchDoneDialog: View { OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) } else { ContextualDaxDialogContent( - message: message, + message: NSAttributedString(string: message), customActionView: AnyView( OnboardingCTAButton(title: cta) { gotItAction() @@ -185,7 +185,7 @@ struct OnboardingTrackersDoneDialog: View { struct OnboardingFinalDialog: View { let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenTitle - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage) + let message: String let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton let highFiveAction: () -> Void @@ -196,7 +196,7 @@ struct OnboardingFinalDialog: View { ContextualDaxDialogContent( title: title, titleFont: Font(UIFont.daxTitle3()), - message: message, + message: NSAttributedString(string: message), customActionView: AnyView( OnboardingCTAButton( title: cta, @@ -226,7 +226,7 @@ struct OnboardingCTAButton: View { // MARK: - Preview #Preview("Try Search") { - OnboardingTrySearchDialog(viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) + OnboardingTrySearchDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage, viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) .padding() } @@ -248,12 +248,12 @@ struct OnboardingCTAButton: View { } #Preview("First Search Dialog") { - OnboardingFirstSearchDoneDialog(shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) + OnboardingFirstSearchDoneDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage, shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) .padding() } #Preview("Final Dialog") { - OnboardingFinalDialog(highFiveAction: {}) + OnboardingFinalDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, highFiveAction: {}) .padding() } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index 2681a5f0de..b4ff585bc7 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -30,15 +30,18 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { private var delegate: OnboardingNavigationDelegate? private let contextualOnboardingLogic: ContextualOnboardingLogic private let onboardingPixelReporter: OnboardingPixelReporting + private let onboardingManager: OnboardingHighlightsManaging init( delegate: OnboardingNavigationDelegate?, contextualOnboardingLogic: ContextualOnboardingLogic, - onboardingPixelReporter: OnboardingPixelReporting + onboardingPixelReporter: OnboardingPixelReporting, + onboardingManager: OnboardingHighlightsManaging = OnboardingManager() ) { self.delegate = delegate self.contextualOnboardingLogic = contextualOnboardingLogic self.onboardingPixelReporter = onboardingPixelReporter + self.onboardingManager = onboardingManager } @ViewBuilder @@ -60,8 +63,9 @@ 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 return FadeInView { - OnboardingTrySearchDialog(viewModel: viewModel) + OnboardingTrySearchDialog(message: message, viewModel: viewModel) .onboardingDaxDialogStyle() } .onboardingContextualBackgroundStyle() @@ -92,8 +96,10 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { } private func createFinalDialog(onDismiss: @escaping () -> Void) -> some View { - FadeInView { - OnboardingFinalDialog(highFiveAction: { + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + + return FadeInView { + OnboardingFinalDialog(message: message, highFiveAction: { onDismiss() }) .onboardingDaxDialogStyle() diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index 60dfeefccf..570d4dc93e 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -48,17 +48,20 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { private let contextualOnboardingSettings: ContextualOnboardingSettings private let contextualOnboardingPixelReporter: OnboardingPixelReporting private let contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding + private let onboardingManager: OnboardingHighlightsManaging init( contextualOnboardingLogic: ContextualOnboardingLogic, contextualOnboardingSettings: ContextualOnboardingSettings = DefaultDaxDialogsSettings(), contextualOnboardingPixelReporter: OnboardingPixelReporting, - contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding = OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding = OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), + onboardingManager: OnboardingHighlightsManaging = OnboardingManager() ) { self.contextualOnboardingSettings = contextualOnboardingSettings self.contextualOnboardingLogic = contextualOnboardingLogic self.contextualOnboardingPixelReporter = contextualOnboardingPixelReporter self.contextualOnboardingSiteSuggestionsProvider = contextualOnboardingSiteSuggestionsProvider + self.onboardingManager = onboardingManager } func makeView(for spec: DaxDialogs.BrowsingSpec, delegate: ContextualOnboardingDelegate, onSizeUpdate: @escaping () -> Void) -> UIHostingController { @@ -122,7 +125,9 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } } - return OnboardingFirstSearchDoneDialog(shouldFollowUp: shouldFollowUpToWebsiteSearch, viewModel: viewModel, gotItAction: gotItAction) + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage + + return OnboardingFirstSearchDoneDialog(message: message, shouldFollowUp: shouldFollowUpToWebsiteSearch, viewModel: viewModel, gotItAction: gotItAction) .onFirstAppear { [weak self] in self?.contextualOnboardingPixelReporter.trackScreenImpression(event: afterSearchPixelEvent) } @@ -164,7 +169,9 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } private func endOfJourneyDialog(delegate: ContextualOnboardingDelegate, pixelName: Pixel.Event) -> some View { - OnboardingFinalDialog(highFiveAction: { [weak delegate] in + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + + return OnboardingFinalDialog(message: message, highFiveAction: { [weak delegate] in delegate?.didTapDismissContextualOnboardingAction() }) .onFirstAppear { [weak self] in diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift new file mode 100644 index 0000000000..5e2e713a7e --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift @@ -0,0 +1,52 @@ +// +// OnboardingIntroViewModel+Copy.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 + +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 + ) + + 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 123fb7eda4..8724b06879 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -24,6 +24,7 @@ import class UIKit.UIApplication final class OnboardingIntroViewModel: ObservableObject { @Published private(set) var state: OnboardingView.ViewState = .landing + let copy: Copy var onCompletingOnboardingIntro: (() -> Void)? private var introSteps: [OnboardingIntroStep] @@ -47,6 +48,8 @@ final class OnboardingIntroViewModel: ObservableObject { } else { OnboardingIntroStep.defaultFlow } + + copy = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default } func onAppear() { diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift index bd6611c0df..0772d9e3b8 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift @@ -25,12 +25,20 @@ extension OnboardingView { struct BrowsersComparisonContent: View { + private let title: String private var animateText: Binding private var showContent: Binding private let setAsDefaultBrowserAction: () -> Void private let cancelAction: () -> Void - init(animateText: Binding = .constant(true), showContent: Binding = .constant(false), setAsDefaultBrowserAction: @escaping () -> Void, cancelAction: @escaping () -> Void) { + init( + title: String, + animateText: Binding = .constant(true), + showContent: Binding = .constant(false), + setAsDefaultBrowserAction: @escaping () -> Void, + cancelAction: @escaping () -> Void + ) { + self.title = title self.animateText = animateText self.showContent = showContent self.setAsDefaultBrowserAction = setAsDefaultBrowserAction @@ -39,7 +47,7 @@ extension OnboardingView { var body: some View { VStack(spacing: 16.0) { - AnimatableTypingText(UserText.DaxOnboardingExperiment.BrowsersComparison.title, startAnimating: animateText) { + AnimatableTypingText(title, startAnimating: animateText) { withAnimation { showContent.wrappedValue = true } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift index 5652d6237c..430be926ea 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift @@ -25,11 +25,13 @@ extension OnboardingView { struct IntroDialogContent: View { + private let title: String private var animateText: Binding private var showCTA: Binding private let action: () -> Void - init(animateText: Binding = .constant(true), showCTA: Binding = .constant(false), action: @escaping () -> Void) { + init(title: String, animateText: Binding = .constant(true), showCTA: Binding = .constant(false), action: @escaping () -> Void) { + self.title = title self.animateText = animateText self.showCTA = showCTA self.action = action @@ -37,7 +39,7 @@ extension OnboardingView { var body: some View { VStack(spacing: 24.0) { - AnimatableTypingText(UserText.DaxOnboardingExperiment.Intro.title, startAnimating: animateText) { + AnimatableTypingText(title) { withAnimation { showCTA.wrappedValue = true } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 4cfba762c8..64a4d56622 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -126,7 +126,11 @@ struct OnboardingView: View { } private var introView: some View { - IntroDialogContent(animateText: $animateIntroText, showCTA: $showIntroButton) { + IntroDialogContent( + title: model.copy.introTitle, + animateText: $animateIntroText, + showCTA: $showIntroButton + ) { animateBrowserComparisonViewState() } .onboardingDaxDialogStyle() @@ -135,6 +139,7 @@ struct OnboardingView: View { private var browsersComparisonView: some View { BrowsersComparisonContent( + title: model.copy.browserComparisonTitle, animateText: $animateComparisonText, showContent: $showComparisonButton, setAsDefaultBrowserAction: { diff --git a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift index 4d3c15a06c..ca4b4fc628 100644 --- a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift +++ b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift @@ -22,18 +22,31 @@ import Onboarding struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding { private let countryAndLanguageProvider: OnboardingRegionAndLanguageProvider + private let onboardingManager: OnboardingHighlightsManaging - init(countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current) { + init( + countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current, + onboardingManager: OnboardingHighlightsManaging = OnboardingManager() + ) { self.countryAndLanguageProvider = countryAndLanguageProvider + self.onboardingManager = onboardingManager } var list: [ContextualOnboardingListItem] { - return [ - option1, - option2, - option3, - surpriseMe - ] + if onboardingManager.isOnboardingHighlightsEnabled { + [ + option1, + option2, + surpriseMe + ] + } else { + [ + option1, + option2, + option3, + surpriseMe + ] + } } private var country: String? { @@ -69,12 +82,14 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding } private var surpriseMe: ContextualOnboardingListItem { - var search: String - if country == "us" { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish + let search = if onboardingManager.isOnboardingHighlightsEnabled { + UserText.HighlightsOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMe } else { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeInternational + country == "us" ? + UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish : + UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeInternational } + return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) } diff --git a/DuckDuckGoTests/BrowserComparisonModelTests.swift b/DuckDuckGoTests/BrowserComparisonModelTests.swift new file mode 100644 index 0000000000..846c986f20 --- /dev/null +++ b/DuckDuckGoTests/BrowserComparisonModelTests.swift @@ -0,0 +1,146 @@ +// +// BrowserComparisonModelTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo + +final class 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 + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockThirdPartyTrackers })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureBlockThirdPartyTrackersIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // 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 + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCookiePopups })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureBlockCookiePopupsIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // 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) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureBlockCreepyAdsIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCreepyAds })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.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) + } + +} diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 500ecb0ff9..520ded80cf 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -21,11 +21,11 @@ import XCTest @testable import DuckDuckGo final class OnboardingIntroViewModelTests: XCTestCase { - private var onboardingManager: OnboardingHighlightsManagerMock! + private var onboardingManager: OnboardingManagerMock! override func setUpWithError() throws { try super.setUpWithError() - onboardingManager = OnboardingHighlightsManagerMock() + onboardingManager = OnboardingManagerMock() } override func tearDownWithError() throws { @@ -339,6 +339,56 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackChooseBrowserCTAAction) } + // MARK: - Copy + + func testWhenIsNotHighlightsThenIntroTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.introTitle + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.Intro.title) + } + + func testWhenIsHighlightsThenIntroTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), 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: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.browserComparisonTitle + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.title) + } + + func testWhenIsHighlightsThenBrowserComparisonTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.browserComparisonTitle + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.title) + } + } private final class OnboardingIntroPixelReporterMock: OnboardingIntroPixelReporting { @@ -358,7 +408,3 @@ private final class OnboardingIntroPixelReporterMock: OnboardingIntroPixelReport didCallTrackChooseBrowserCTAAction = true } } - -private class OnboardingHighlightsManagerMock: OnboardingHighlightsManaging { - var isOnboardingHighlightsEnabled: Bool = false -} diff --git a/DuckDuckGoTests/OnboardingManagerMock.swift b/DuckDuckGoTests/OnboardingManagerMock.swift new file mode 100644 index 0000000000..9322299bfe --- /dev/null +++ b/DuckDuckGoTests/OnboardingManagerMock.swift @@ -0,0 +1,25 @@ +// +// OnboardingManagerMock.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo + +final class OnboardingManagerMock: OnboardingHighlightsManaging { + var isOnboardingHighlightsEnabled: Bool = false +} diff --git a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift index fbf73bf6ff..59456d6edc 100644 --- a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift +++ b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift @@ -22,12 +22,23 @@ import Onboarding @testable import DuckDuckGo class OnboardingSuggestedSearchesProviderTests: XCTestCase { - + private var onboardingManagerMock: OnboardingManagerMock! let userText = UserText.DaxOnboardingExperiment.ContextualOnboarding.self + let highlightsUserText = UserText.HighlightsOnboardingExperiment.ContextualOnboarding.self + + override func setUpWithError() throws { + try super.setUpWithError() + onboardingManagerMock = OnboardingManagerMock() + } + + override func tearDownWithError() throws { + onboardingManagerMock = nil + try super.tearDownWithError() + } func testSearchesListForEnglishLanguageAndUsRegion() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), @@ -41,7 +52,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForNonEnglishLanguageAndNonUSRegion() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), @@ -55,7 +66,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForUSRegionAndNonEnglishLanguage() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), @@ -66,6 +77,51 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { 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: 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: 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: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + } class MockOnboardingRegionAndLanguageProvider: OnboardingRegionAndLanguageProvider {