From 38f7684ce63e77b54c749277442e547637bef99b Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 9 Sep 2024 11:25:34 +1000 Subject: [PATCH 01/31] Add copy for onboarding highlights experiment (#3325) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208084960726983/f **Description**: Update the copy for Highlights experiment --- DuckDuckGo/UserText.swift | 46 ++++++++++++++++++++- DuckDuckGo/en.lproj/Localizable.strings | 55 ++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 179a31c84e..2c93e84613 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1327,9 +1327,9 @@ But if you *do* want a peek under the hood, you can find more information about enum Features { public static let privateSearch = NSLocalizedString("onboarding.browsers.features.privateSearch.title", value: "Search privately by default", comment: "Message to highlight browser capability of private searches") public static let trackerBlockers = NSLocalizedString("onboarding.browsers.features.trackerBlocker.title", value: "Block 3rd-party trackers", comment: "Message to highlight browser capability ofblocking 3rd party trackers") - public static let cookiePopups = NSLocalizedString("onboarding.browsers.features.cookiePopups.title", value: "Block cookie pop-ups", comment: "Message to highlight browser capability of blocking cookie pop-ups") + public static let 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 ofswiftly erase browsing data") + 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") } } @@ -1358,4 +1358,46 @@ But if you *do* want a peek under the hood, you can find more information about 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.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.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") + public static let eraseBrowsingData = NSLocalizedString("onboarding.highlights.browsers.features.eraseBrowsingData.title", value: "Erase browsing data swiftly", comment: "Message to highlight browser capability of swiftly erase browsing data") + } + } + + enum AppIconSelection { + public static let title = NSLocalizedString("onboarding.highlights.appIconSelection.title", value: "Which color looks best on me?", comment: "The title of the onboarding dialog popup to select the preferred App icon.") + public static let message = NSLocalizedString("onboarding.highlights.appIconSelection.message", value: "Pick your app icon:", comment: "The subheader of the onboarding dialog popup to select the preferred App icon.") + public static let cta = NSLocalizedString("onboarding.highlights.appIconSelection.cta", value: "Next", comment: "The title of the CTA to progress to the next onboarding screen.") + } + + enum AddressBarPosition { + public static let title = NSLocalizedString("onboarding.highlights.addressBarPosition.title", value: "Where should I put your address bar?", comment: "The title of the onboarding dialog popup to select the preferred address bar position.") + public static let topTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.top.title", value: "Top (Default)", comment: "The title of the option to set the address bar to the top.") + public static let topMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.top.message", value: "Easy to see", comment: "The message of the option to set the address bar to the top.") + public static let bottomTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.title", value: "Bottom", comment: "The title of the option to set the address bar to the bottom.") + public static let bottomMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.message", value: "Easy to reach", comment: "The message of the option to set the address bar to the bottom.") + public static let cta = NSLocalizedString("onboarding.highlights.addressBarPosition.cta", value: "Next", comment: "The title of the CTA to progress to the next onboarding screen.") + } + + enum ContextualOnboarding { + 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 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.") + } + + enum FireDialog { + public static let skip = NSLocalizedString("onboarding.highlights.fireDialog.cta.skip", value: "Skip", comment: "The title of the fire button CTA to skip erasing the data.") + } + } } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index a68ea3a7d8..9ca7809f77 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -770,6 +770,15 @@ /* 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!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Remember: every time you browse with me a creepy ad loses its wings."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "That’s DuckDuckGo Search! Private. Fast. Fewer ads."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Your DuckDuckGo searches are always private."; + /* 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!"; @@ -1811,13 +1820,13 @@ https://duckduckgo.com/mac"; /* Button to change the default browser */ "onboarding.browsers.cta" = "Choose Your Browser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* 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 ofswiftly erase browsing data */ +/* 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 */ @@ -1829,6 +1838,48 @@ https://duckduckgo.com/mac"; /* 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"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Bottom"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Next"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Where should I put your address bar?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Easy to see"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Top (Default)"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Next"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Pick your app icon:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Which color looks best on me?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Block cookie requests & popups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Block targeted ads"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Erase browsing data swiftly"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Skip"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hi there.\n\nReady for a faster browser that keeps you protected?"; + /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Let’s do it!"; From d33a288484563ecba401d4d48b878831baca9b83 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 9 Sep 2024 12:17:49 +1000 Subject: [PATCH 02/31] Add Onboarding Progress bar (#3323) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208108104325247 **Description**: 1. Add Progress Bar to Onboarding. 2. Add view logic to show/hide the progress bar for onboarding highlights and normal flow. --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../OnboardingIntroViewModel.swift | 54 +++++- .../OnboardingIntro/OnboardingView.swift | 78 ++++++++- .../ProgressBarView.swift | 159 ++++++++++++++++++ .../Styles/OnboardingTextStyles.swift | 24 ++- .../OnboardingIntroViewModelTests.swift | 133 +++++++++++++-- 6 files changed, 427 insertions(+), 25 deletions(-) create mode 100644 DuckDuckGo/OnboardingExperiment/ProgressBarView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 425c8d8966..cf066c0c8b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -712,6 +712,7 @@ 9FB893F82C784A1700332E5E /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 9FB893F72C784A1700332E5E /* Onboarding */; }; 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */; }; 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; + 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.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 */; }; @@ -2483,6 +2484,7 @@ 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModel.swift; sourceTree = ""; }; 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LaunchOptionsHandler.swift; path = ../DuckDuckGo/LaunchOptionsHandler.swift; sourceTree = ""; }; 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandlerTests.swift; sourceTree = ""; }; + 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.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 = ""; }; @@ -4756,6 +4758,7 @@ 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, 9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */, 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, + 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */, ); path = OnboardingExperiment; sourceTree = ""; @@ -7578,6 +7581,7 @@ 8505836A219F424500ED4EDB /* UIAlertControllerExtension.swift in Sources */, C12726F22A5FF8CB00215B02 /* EmailSignupPromptViewController.swift in Sources */, 983EABB8236198F6003948D1 /* DatabaseMigration.swift in Sources */, + 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */, 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */, 314C92B827C3DD660042EC96 /* QuickLookPreviewView.swift in Sources */, 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */, diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index ffda3bda2b..15839ee5ab 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -25,6 +25,8 @@ final class OnboardingIntroViewModel: ObservableObject { @Published private(set) var state: OnboardingView.ViewState = .landing var onCompletingOnboardingIntro: (() -> Void)? + private var introSteps: [OnboardingIntroStep] + private let pixelReporter: OnboardingIntroPixelReporting private let onboardingManager: OnboardingHighlightsManaging private let urlOpener: URLOpener @@ -37,15 +39,16 @@ final class OnboardingIntroViewModel: ObservableObject { self.pixelReporter = pixelReporter self.onboardingManager = onboardingManager self.urlOpener = urlOpener + introSteps = onboardingManager.isOnboardingHighlightsEnabled ? OnboardingIntroStep.highlightsFlow : OnboardingIntroStep.defaultFlow } func onAppear() { - state = .onboarding(.startOnboardingDialog) + state = makeViewState(for: .introDialog) pixelReporter.trackOnboardingIntroImpression() } func startOnboardingAction() { - state = .onboarding(.browsersComparisonDialog) + state = makeViewState(for: .browserComparison) pixelReporter.trackBrowserComparisonImpression() } @@ -63,7 +66,10 @@ final class OnboardingIntroViewModel: ObservableObject { } func appIconPickerContinueAction() { - // TODO: Remove below and implement proper logic + state = makeViewState(for: .addressBarPositionSelection) + } + + func selectAddressBarPositionAction() { onCompletingOnboardingIntro?() } @@ -73,12 +79,52 @@ final class OnboardingIntroViewModel: ObservableObject { 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 + } + + // 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) + } + + let viewState = switch introStep { + case .introDialog: + OnboardingView.ViewState.onboarding(.init(type: .startOnboardingDialog, step: .hidden)) + case .browserComparison: + OnboardingView.ViewState.onboarding(.init(type: .browsersComparisonDialog, step: stepInfo())) + case .appIconSelection: + OnboardingView.ViewState.onboarding(.init(type: .chooseAppIconDialog, step: stepInfo())) + case .addressBarPositionSelection: + OnboardingView.ViewState.onboarding(.init(type: .chooseAddressBarPositionDialog, step: stepInfo())) + } + + return viewState + } + func handleSetDefaultBrowserAction() { if onboardingManager.isOnboardingHighlightsEnabled { - state = .onboarding(.chooseAppIconDialog) + state = makeViewState(for: .appIconSelection) } else { onCompletingOnboardingIntro?() } } } + +// MARK: - OnboardingIntroStep + +private enum OnboardingIntroStep { + case introDialog + case browserComparison + case appIconSelection + case addressBarPositionSelection + + static let defaultFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison] + static let highlightsFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 2af2121b7a..02f280319f 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -19,6 +19,7 @@ import SwiftUI import Onboarding +import struct DuckUI.PrimaryButtonStyle // MARK: - OnboardingView @@ -64,11 +65,11 @@ struct OnboardingView: View { showDialogBox: $showDaxDialogBox, onTapGesture: { withAnimation { - switch model.state { - case .onboarding(.startOnboardingDialog): + switch model.state.intro?.type { + case .startOnboardingDialog: showIntroButton = true animateIntroText = false - case .onboarding(.browsersComparisonDialog): + case .browsersComparisonDialog: showComparisonButton = true animateComparisonText = false default: break @@ -77,17 +78,20 @@ struct OnboardingView: View { }, content: { VStack { - switch state { + switch state.type { case .startOnboardingDialog: introView case .browsersComparisonDialog: browsersComparisonView case .chooseAppIconDialog: appIconPickerView + case .chooseAddressBarPositionDialog: + addressBarPreferenceSelectionView } } } ) + .onboardingProgressIndicator(currentStep: state.step.currentStep, totalSteps: state.step.totalSteps) } .frame(width: geometry.size.width, alignment: .center) .offset(y: geometry.size.height * Metrics.dialogVerticalOffsetPercentage.build(v: verticalSizeClass, h: horizontalSizeClass)) @@ -136,13 +140,27 @@ struct OnboardingView: View { } private var appIconPickerView: some View { - // TODO: Implement AppIconPicker + // TODO: Implement View VStack(spacing: 30) { Text(verbatim: "Choose App Icon") Button(action: model.appIconPickerContinueAction) { - Text(verbatim: "Continue") + Text(verbatim: "Next") } + .buttonStyle(PrimaryButtonStyle()) + } + .onboardingDaxDialogStyle() + } + + private var addressBarPreferenceSelectionView: some View { + // TODO: Implement View + VStack(spacing: 30) { + Text(verbatim: "Choose Address Bar Position") + + Button(action: model.selectAddressBarPositionAction) { + Text(verbatim: "Next") + } + .buttonStyle(PrimaryButtonStyle()) } .onboardingDaxDialogStyle() } @@ -181,18 +199,44 @@ extension OnboardingView { enum ViewState: Equatable { case landing case onboarding(Intro) + + var intro: Intro? { + switch self { + case .landing: + return nil + case let .onboarding(intro): + return intro + } + } } } extension OnboardingView.ViewState { + + struct Intro: Equatable { + let type: IntroType + let step: StepInfo + } - enum Intro: Equatable { +} + +extension OnboardingView.ViewState.Intro { + + enum IntroType: Equatable { case startOnboardingDialog case browsersComparisonDialog case chooseAppIconDialog + case chooseAddressBarPositionDialog } - + + struct StepInfo: Equatable { + let currentStep: Int + let totalSteps: Int + + static let hidden = StepInfo(currentStep: 0, totalSteps: 0) + } + } // MARK: - Metrics @@ -202,6 +246,24 @@ private enum Metrics { static let daxDialogVisibilityDelay: TimeInterval = 0.5 static let comparisonChartAnimationDuration = 0.25 static let dialogVerticalOffsetPercentage = MetricBuilder(value: 0.1).smallIphone(0.01) + static let progressBarTrailingPadding: CGFloat = 16.0 + static let progressBarTopPadding: CGFloat = 12.0 +} + +// MARK: - Helpers + +private extension View { + + func onboardingProgressIndicator(currentStep: Int, totalSteps: Int) -> some View { + overlay(alignment: .topTrailing) { + OnboardingProgressIndicator(stepInfo: .init(currentStep: currentStep, totalSteps: totalSteps)) + .padding(.trailing, Metrics.progressBarTrailingPadding) + .padding(.top, Metrics.progressBarTopPadding) + .transition(.identity) + .visibility(totalSteps == 0 ? .invisible : .visible) + } + } + } // MARK: - Preview diff --git a/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift b/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift new file mode 100644 index 0000000000..cebcc13db3 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift @@ -0,0 +1,159 @@ +// +// ProgressBarView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OnboardingProgressIndicator: View { + + struct StepInfo { + let currentStep: Int + let totalSteps: Int + + fileprivate var percentage: Double { + guard totalSteps > 0 else { return 0 } + return Double(currentStep) / Double(totalSteps) * 100 + } + } + + let stepInfo: StepInfo + + var body: some View { + VStack(spacing: OnboardingProgressMetrics.verticalSpacing) { + HStack { + Spacer() + Text("\(stepInfo.currentStep) / \(stepInfo.totalSteps)") + .onboardingProgressTitleStyle() + .padding(.trailing, OnboardingProgressMetrics.textPadding) + } + ProgressBarView(progress: stepInfo.percentage) + .frame(width: OnboardingProgressMetrics.progressBarSize.width, height: OnboardingProgressMetrics.progressBarSize.height) + } + .fixedSize() + } +} + +private enum OnboardingProgressMetrics { + static let verticalSpacing: CGFloat = 8 + static let textPadding: CGFloat = 4 + static let progressBarSize = CGSize(width: 64, height: 4) +} + +struct ProgressBarView: View { + @Environment(\.colorScheme) private var colorScheme + + let progress: Double + + var body: some View { + Capsule() + .foregroundStyle(backgroundColor) + .overlay( + GeometryReader { proxy in + ProgressBarGradient() + .clipShape(Capsule().inset(by: ProgressBarMetrics.strokeWidth / 2)) + .frame(width: progress * proxy.size.width / 100) + .animation(.easeInOut, value: progress) + } + ) + .overlay( + Capsule() + .stroke(borderColor, lineWidth: ProgressBarMetrics.strokeWidth) + ) + } + + private var backgroundColor: Color { + colorScheme == .light ? ProgressBarMetrics.backgroundLight : ProgressBarMetrics.backgroundDark + } + + private var borderColor: Color { + colorScheme == .light ? ProgressBarMetrics.borderLight : ProgressBarMetrics.borderDark + } + +} + +private enum ProgressBarMetrics { + static let backgroundLight: Color = .black.opacity(0.06) + static let borderLight: Color = .black.opacity(0.18) + static let backgroundDark: Color = .white.opacity(0.09) + static let borderDark: Color = .white.opacity(0.18) + static let strokeWidth: CGFloat = 1 +} + +struct ProgressBarGradient: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + let colors: [Color] + switch colorScheme { + case .light: + colors = lightGradientColors + case .dark: + colors = darkGradientColors + @unknown default: + colors = lightGradientColors + } + + return LinearGradient( + colors: colors, + startPoint: .leading, + endPoint: .trailing + ) + } + + private var lightGradientColors: [Color] { + [ + .init(0x3969EF, alpha: 1.0), + .init(0x6B4EBA, alpha: 1.0), + .init(0xDE5833, alpha: 1.0), + ] + } + + private var darkGradientColors: [Color] { + [ + .init(0x3969EF, alpha: 1.0), + .init(0x6B4EBA, alpha: 1.0), + .init(0xDE5833, alpha: 1.0), + ] + } +} + +#Preview("Onboarding Progress Indicator") { + struct PreviewWrapper: View { + @State var stepInfo = OnboardingProgressIndicator.StepInfo(currentStep: 1, totalSteps: 3) + + var body: some View { + VStack(spacing: 100) { + OnboardingProgressIndicator(stepInfo: stepInfo) + + Button(action: { + let nextStep = stepInfo.currentStep < stepInfo.totalSteps ? stepInfo.currentStep + 1 : 1 + stepInfo = OnboardingProgressIndicator.StepInfo(currentStep: nextStep, totalSteps: stepInfo.totalSteps) + }, label: { + Text("Update Progress") + }) + } + } + } + + return PreviewWrapper() +} + +#Preview("Progress Bar") { + ProgressBarView(progress: 80) + .frame(width: 200, height: 8) +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift index 283c8cff3a..113ea7c39e 100644 --- a/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift +++ b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift @@ -30,7 +30,7 @@ extension OnboardingStyles { func body(content: Content) -> some View { let view = content .font(.system(size: fontSize, weight: .bold)) - .foregroundColor(.primary) + .foregroundStyle(Color.primary) .multilineTextAlignment(.center) if #available(iOS 16, *) { @@ -42,6 +42,22 @@ extension OnboardingStyles { } + struct ProgressBarTitleStyle: ViewModifier { + + func body(content: Content) -> some View { + let view = content + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(Color.secondary) + + if #available(iOS 16, *) { + return view.kerning(0.06) + } else { + return view + } + } + + } + } extension View { @@ -49,5 +65,9 @@ extension View { func onboardingTitleStyle(fontSize: CGFloat) -> some View { modifier(OnboardingStyles.TitleStyle(fontSize: fontSize)) } - + + func onboardingProgressTitleStyle() -> some View { + modifier(OnboardingStyles.ProgressBarTitleStyle()) + } + } diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index f3ad300002..0d12240db5 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -21,12 +21,23 @@ import XCTest @testable import DuckDuckGo final class OnboardingIntroViewModelTests: XCTestCase { + private var onboardingManager: OnboardingHighlightsManagerMock! + + override func setUpWithError() throws { + try super.setUpWithError() + onboardingManager = OnboardingHighlightsManagerMock() + } + + override func tearDownWithError() throws { + onboardingManager = nil + try super.tearDownWithError() + } // MARK: - State + Actions func testWhenSubscribeToViewStateThenShouldSendLanding() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.state @@ -37,32 +48,32 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenOnAppearIsCalledThenViewStateChangesToStartOnboardingDialog() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN sut.onAppear() // THEN - XCTAssertEqual(sut.state, .onboarding(.startOnboardingDialog)) + XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } func testWhenStartOnboardingActionIsCalledThenViewStateChangesToBrowsersComparisonDialog() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) XCTAssertEqual(sut.state, .landing) // WHEN sut.startOnboardingAction() // THEN - XCTAssertEqual(sut.state, .onboarding(.browsersComparisonDialog)) + XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .hidden))) } func testWhenSetDefaultBrowserActionIsCalledThenURLOpenerOpensSettingsURL() { // GIVEN let urlOpenerMock = MockURLOpener() - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: urlOpenerMock) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: urlOpenerMock) XCTAssertFalse(urlOpenerMock.didCallOpenURL) XCTAssertNil(urlOpenerMock.capturedURL) @@ -77,7 +88,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -93,7 +104,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -106,12 +117,108 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(didCallOnCompletingOnboardingIntro) } + // MARK: - Highlights State + Actions + + func testWhenSubscribeToViewStateAndIsHighlightsFlowThenShouldSendLanding() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.state + + // THEN + XCTAssertEqual(result, .landing) + } + + func testWhenOnAppearIsCalledAndIsHighlightsFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) + } + + func testWhenStartOnboardingActionIsCalledAndIsHighlightsFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 3)))) + } + + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) + } + + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.cancelSetDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) + } + + func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAddressBarPositionDialog, step: .init(currentStep: 3, totalSteps: 3)))) + } + + func testWhenSelectAddressBarPositionActionIsCalledAndIsHighlightsFlowThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.selectAddressBarPositionAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + // MARK: - Pixels func testWhenOnAppearIsCalledThenPixelReporterTrackOnboardingIntroImpression() { // GIVEN let pixelReporterMock = OnboardingIntroPixelReporterMock() - let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackOnboardingIntroImpression) // WHEN @@ -124,7 +231,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStartOnboardingActionIsCalledThenPixelReporterTrackBrowserComparisonImpression() { // GIVEN let pixelReporterMock = OnboardingIntroPixelReporterMock() - let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackBrowserComparisonImpression) // WHEN @@ -137,7 +244,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenChooseBrowserIsCalledThenPixelReporterTrackChooseBrowserCTAAction() { // GIVEN let pixelReporterMock = OnboardingIntroPixelReporterMock() - let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackChooseBrowserCTAAction) // WHEN @@ -166,3 +273,7 @@ private final class OnboardingIntroPixelReporterMock: OnboardingIntroPixelReport didCallTrackChooseBrowserCTAAction = true } } + +private class OnboardingHighlightsManagerMock: OnboardingHighlightsManaging { + var isOnboardingHighlightsEnabled: Bool = false +} From 9192f2410f61adaf3ee104e0d52fcedcc706661c Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 9 Sep 2024 11:48:34 +0200 Subject: [PATCH 03/31] New feature flag for autofillSurveys (#3318) Task/Issue URL: https://app.asana.com/0/1201462886803403/1208215340836293/f Tech Design URL: CC: **Description**: Adds new feature flag for autofillSurveys --- Core/FeatureFlag.swift | 3 +++ DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 0fe7590da2..ce78a78286 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -41,6 +41,7 @@ public enum FeatureFlag: String { case syncPromotionBookmarks case syncPromotionPasswords case onboardingHighlights + case autofillSurveys } extension FeatureFlag: FeatureFlagSourceProviding { @@ -86,6 +87,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(SyncPromotionSubfeature.passwords)) case .onboardingHighlights: return .internalOnly + case .autofillSurveys: + return .remoteReleasable(.feature(.autofillSurveys)) } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cf066c0c8b..22dccb3a47 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10713,7 +10713,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 191.2.1; + version = 191.2.2; }; }; 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 4b30096f7b..f09f01cf70 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" : "6cb645ff85948eadde86365483792d75845270fe", - "version" : "191.2.1" + "revision" : "660971f355c2288ebd3041e92d9674abd9e3a3e0", + "version" : "191.2.2" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" From b014c9c1bfb805209916b1d2b5dd4c8512a78d4d Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 9 Sep 2024 16:17:17 +0200 Subject: [PATCH 04/31] Duckplayer Experiment Override Fix (#3329) Task/Issue URL: https://app.asana.com/0/1204099484721401/1208239844031863/f Description: Fixes experiment reset logic. - Just re-enrolls you now. --- .../DuckPlayer/DuckPlayerLaunchExperiment.swift | 15 ++++----------- DuckDuckGo/RootDebugViewController.swift | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift index 7206c2b018..b809c7d5b9 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift @@ -91,15 +91,6 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { @UserDefaultsWrapper(key: .duckPlayerPixelExperimentCohort, defaultValue: nil) var experimentCohort: String? - @UserDefaultsWrapper(key: .duckPlayerPixelExperimentOverride, defaultValue: false) - var experimentOverride: Bool { - didSet { - if experimentOverride { - enrollmentDate = Date() - } - } - } - enum Cohort: String { case control case experiment @@ -144,7 +135,7 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { } var isExperimentCohort: Bool { - return experimentCohort == "experiment" || experimentOverride + return experimentCohort == "experiment" } func assignUserToCohort() { @@ -226,12 +217,14 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { lastDayPixelFired = nil lastWeekPixelFired = nil lastVideoIDReported = nil - experimentOverride = false } func override() { enrollmentDate = Date() experimentCohort = "experiment" + lastDayPixelFired = nil + lastWeekPixelFired = nil + lastVideoIDReported = nil } diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index a07df2d5c0..6f861725a0 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -187,7 +187,7 @@ class RootDebugViewController: UITableViewController { DuckPlayerLaunchExperiment().cleanup() ActionMessageView.present(message: "Experiment Settings deleted. You'll be assigned a random cohort") case .overrideDuckPlayerExperiment: - DuckPlayerLaunchExperiment().experimentOverride = true + DuckPlayerLaunchExperiment().override() ActionMessageView.present(message: "Overriding experiment. You are now in the 'experiment' group. Restart the app to complete") } } From b8bf20525b9c888b905a6b68c6fa1eed95b50c79 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 9 Sep 2024 10:35:16 -0700 Subject: [PATCH 05/31] Update VPN user agent (#3302) Task/Issue URL: https://app.asana.com/0/1207162258152427/1207484485195971/f Tech Design URL: CC: Description: This PR updates the user agent of the VPN to include the version number, not just build number. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../NetworkProtectionPacketTunnelProvider.swift | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 22dccb3a47..db6bfd8eaa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10713,7 +10713,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 191.2.2; + version = 191.2.3; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f09f01cf70..81fa28071f 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" : "660971f355c2288ebd3041e92d9674abd9e3a3e0", - "version" : "191.2.2" + "revision" : "b83ccf14b5844e8876de04bcc3074a15569f3b26", + "version" : "191.2.3" } }, { diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 56a504e774..71069a06fe 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -318,6 +318,7 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { } @objc init() { + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) From acc4be3596c0b3bed9a9da225542178ebf887c25 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 9 Sep 2024 22:23:22 +0200 Subject: [PATCH 06/31] Release 7.137.0-0 (#3339) Task/Issue URL: https://app.asana.com/0/inbox/1202926619868495/1208264707273639/1208264848712956/f Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index db6bfd8eaa..0c0703e228 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8928,7 +8928,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8965,7 +8965,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9055,7 +9055,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9082,7 +9082,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9231,7 +9231,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9256,7 +9256,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9325,7 +9325,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9359,7 +9359,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9392,7 +9392,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9422,7 +9422,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9732,7 +9732,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9763,7 +9763,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9791,7 +9791,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9824,7 +9824,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9854,7 +9854,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9887,11 +9887,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10124,7 +10124,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10151,7 +10151,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10183,7 +10183,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10220,7 +10220,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10255,7 +10255,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10290,11 +10290,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10467,11 +10467,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10500,10 +10500,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From d68271f514ae784d81ddd383437fff905b740c87 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 10 Sep 2024 10:10:33 +0200 Subject: [PATCH 07/31] Version and embedded update after script failure --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 37 ++++++++++++------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 6cdb4ec67a..f79c653987 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.136.0 +MARKETING_VERSION = 7.137.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index a049977e45..bf0b7a567d 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"38047fbabac1af7f77112ee692d5d481\"" - public static let embeddedDataSHA = "ee998861bbed8b784a7f19caafd76a8f6eb9a82160b0b6ddb21d97e67332b38f" + public static let embeddedDataETag = "\"9087766799743533c0741b03cea431d1\"" + public static let embeddedDataSHA = "9e9fcfd329fc587ba732cf9cb7e71d81f7af7717c3f804f28b9c8603599ee8d8" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index 9bbcb9cdf4..6330f1c820 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1725043256685, + "version": 1725898107484, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -335,6 +335,12 @@ { "domain": "dkb.de" }, + { + "domain": "headphonecheck.com" + }, + { + "domain": "lehmannaudio.com" + }, { "domain": "marvel.com" }, @@ -382,7 +388,7 @@ } } }, - "hash": "5a81046c452177af48157543487a38d5" + "hash": "a235f7ab6a5a3632380570bbc5113d51" }, "autofillBreakageReporter": { "state": "enabled", @@ -449,12 +455,15 @@ }, { "percent": 25 + }, + { + "percent": 100 } ] } } }, - "hash": "471e5069536f5b7824c5e4b60e5be274" + "hash": "28d4af98382248e184c4315bd49f4222" }, "breakageReporting": { "state": "disabled", @@ -6004,6 +6013,16 @@ } ] }, + "adsrvr.org": { + "rules": [ + { + "rule": "js.adsrvr.org/up_loader.1.1.0.js", + "domains": [ + "codot.gov" + ] + } + ] + }, "adswizz.com": { "rules": [ { @@ -8576,16 +8595,6 @@ } ] }, - "siteimproveanalytics.com": { - "rules": [ - { - "rule": "siteimproveanalytics.com", - "domains": [ - "codot.gov" - ] - } - ] - }, "skimresources.com": { "rules": [ { @@ -9173,7 +9182,7 @@ "domain": "capitalone.com" } ], - "hash": "70bdfe11b5235b40019495340bd91154" + "hash": "96d026a445971b650f4a6e899e339ff8" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 2428aca706..7cb7486a5e 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.136.0 + 7.137.0 Key version Title From 93a1189d3932abfaab66b20db6a9c513ff856d25 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Tue, 10 Sep 2024 10:38:29 +0200 Subject: [PATCH 08/31] Add survey to Passwords screen (#3327) Task/Issue URL: https://app.asana.com/0/1201462886803403/1207935899042226/f Tech Design URL: CC: **Description**: Adds support for presenting a survey in the Passwords screen, controlled via privacy config remote feature flag --- Core/PixelEvent.swift | 10 +- Core/UserDefaultsPropertyWrapper.swift | 1 + DuckDuckGo.xcodeproj/project.pbxproj | 62 +++++++- .../Contents.json | 12 ++ .../Passwords-DDG-96x96.svg | 26 ++++ DuckDuckGo/AutofillDebugViewController.swift | 8 +- DuckDuckGo/AutofillHeaderViewFactory.swift | 92 +++++++++++ DuckDuckGo/AutofillLoginListViewModel.swift | 27 +++- ...ofillLoginSettingsListViewController.swift | 119 +++++++++++---- DuckDuckGo/AutofillSurveyManager.swift | 126 +++++++++++++++ DuckDuckGo/AutofillSurveyView.swift | 100 ++++++++++++ DuckDuckGo/Debug.storyboard | 13 +- .../AutofillHeaderViewFactoryTests.swift | 144 ++++++++++++++++++ .../AutofillLoginListViewModelTests.swift | 79 +++++++++- .../AutofillSurveyManagerTests.swift | 118 ++++++++++++++ 15 files changed, 901 insertions(+), 36 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg create mode 100644 DuckDuckGo/AutofillHeaderViewFactory.swift create mode 100644 DuckDuckGo/AutofillSurveyManager.swift create mode 100644 DuckDuckGo/AutofillSurveyView.swift create mode 100644 DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift create mode 100644 DuckDuckGoTests/AutofillSurveyManagerTests.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index b5b6b5384c..e5cb5bcdf9 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -298,6 +298,8 @@ extension Pixel { case autofillLoginsReportConfirmationPromptConfirmed case autofillLoginsReportConfirmationPromptDismissed + case autofillManagementScreenVisitSurveyAvailable + case getDesktopCopy case getDesktopShare @@ -1105,9 +1107,11 @@ extension Pixel.Event { case .autofillLoginsReportFailure: return "autofill_logins_report_failure" case .autofillLoginsReportAvailable: return "autofill_logins_report_available" - case .autofillLoginsReportConfirmationPromptDisplayed: return "autofill_logins_report_confirmation_prompt_displayed" - case .autofillLoginsReportConfirmationPromptConfirmed: return "autofill_logins_report_confirmation_prompt_confirmed" - case .autofillLoginsReportConfirmationPromptDismissed: return "autofill_logins_report_confirmation_prompt_dismissed" + case .autofillLoginsReportConfirmationPromptDisplayed: return "autofill_logins_report_confirmation_displayed" + case .autofillLoginsReportConfirmationPromptConfirmed: return "autofill_logins_report_confirmation_confirmed" + case .autofillLoginsReportConfirmationPromptDismissed: return "autofill_logins_report_confirmation_dismissed" + + case .autofillManagementScreenVisitSurveyAvailable: return "m_autofill_management_screen_visit_survey_available" case .getDesktopCopy: return "m_get_desktop_copy" case .getDesktopShare: return "m_get_desktop_share" diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 1c1b5216a7..4f221722cc 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -92,6 +92,7 @@ public struct UserDefaultsWrapper { case autofillSearchDauDate = "com.duckduckgo.app.autofill.SearchDauDate" case autofillFillDate = "com.duckduckgo.app.autofill.FillDate" case autofillOnboardedUser = "com.duckduckgo.app.autofill.OnboardedUser" + case autofillSurveysCompleted = "com.duckduckgo.app.autofill.SurveysCompleted" case syncPromoBookmarksDismissed = "com.duckduckgo.app.sync.PromoBookmarksDismissed" case syncPromoPasswordsDismissed = "com.duckduckgo.app.sync.PromoPasswordsDismissed" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0c0703e228..9a57964978 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -850,6 +850,11 @@ C185ED672BD43DA100BAE9DC /* ImportPasswordsStatusHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C185ED632BD438AF00BAE9DC /* ImportPasswordsStatusHandlerTests.swift */; }; C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18ED4392AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift */; }; C18ED43C2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18ED43B2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift */; }; + C1935A0E2C88D11D001AD72D /* AutofillSurveyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A0D2C88D11D001AD72D /* AutofillSurveyView.swift */; }; + C1935A102C88D131001AD72D /* AutofillSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A0F2C88D131001AD72D /* AutofillSurveyManager.swift */; }; + C1935A122C88D1D8001AD72D /* AutofillHeaderViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A112C88D1D8001AD72D /* AutofillHeaderViewFactory.swift */; }; + C1935A222C89CA9F001AD72D /* AutofillSurveyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A212C89CA9F001AD72D /* AutofillSurveyManagerTests.swift */; }; + C1935A242C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A232C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift */; }; C1963863283794A000298D4D /* BookmarksCachingSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1963862283794A000298D4D /* BookmarksCachingSearch.swift */; }; C1B7B51C28941E980098FD6A /* HomeMessageViewModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B51B28941E980098FD6A /* HomeMessageViewModelBuilder.swift */; }; C1B7B52528941F2A0098FD6A /* RemoteMessagingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B52128941F2A0098FD6A /* RemoteMessagingClient.swift */; }; @@ -2619,6 +2624,11 @@ C185ED652BD43A5500BAE9DC /* MockDDGSyncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDDGSyncing.swift; sourceTree = ""; }; C18ED4392AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillSettingsEnableFooterView.swift; sourceTree = ""; }; C18ED43B2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileTextPreviewDebugViewController.swift; sourceTree = ""; }; + C1935A0D2C88D11D001AD72D /* AutofillSurveyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillSurveyView.swift; sourceTree = ""; }; + C1935A0F2C88D131001AD72D /* AutofillSurveyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillSurveyManager.swift; sourceTree = ""; }; + C1935A112C88D1D8001AD72D /* AutofillHeaderViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillHeaderViewFactory.swift; sourceTree = ""; }; + C1935A212C89CA9F001AD72D /* AutofillSurveyManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillSurveyManagerTests.swift; sourceTree = ""; }; + C1935A232C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillHeaderViewFactoryTests.swift; sourceTree = ""; }; C1963862283794A000298D4D /* BookmarksCachingSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksCachingSearch.swift; sourceTree = ""; }; C1B0F6412AB08BE9001EAF05 /* MockPrivacyConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyConfiguration.swift; sourceTree = ""; }; C1B7B51B28941E980098FD6A /* HomeMessageViewModelBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeMessageViewModelBuilder.swift; sourceTree = ""; }; @@ -3509,6 +3519,7 @@ 319A37132829A5450079FBCE /* Table */ = { isa = PBXGroup; children = ( + C1935A0C2C88D101001AD72D /* Survey */, 31CC224828369B38001654A4 /* AutofillLoginSettingsListViewController.swift */, 319A37142829A55F0079FBCE /* AutofillListItemTableViewCell.swift */, 310ECFDC282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift */, @@ -3520,6 +3531,7 @@ C1B924B62ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift */, C1836CE02C359EC90016D057 /* AutofillBreakageReportCellContentView.swift */, C1836CE42C35A0EA0016D057 /* AutofillBreakageReportTableViewCell.swift */, + C1935A112C88D1D8001AD72D /* AutofillHeaderViewFactory.swift */, ); name = Table; sourceTree = ""; @@ -4978,6 +4990,49 @@ name = Import; sourceTree = ""; }; + C1935A0C2C88D101001AD72D /* Survey */ = { + isa = PBXGroup; + children = ( + C1935A0F2C88D131001AD72D /* AutofillSurveyManager.swift */, + C1935A0D2C88D11D001AD72D /* AutofillSurveyView.swift */, + ); + name = Survey; + sourceTree = ""; + }; + C1935A1D2C89CA4B001AD72D /* Management */ = { + isa = PBXGroup; + children = ( + C1935A1E2C89CA53001AD72D /* List */, + F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */, + ); + name = Management; + sourceTree = ""; + }; + C1935A1E2C89CA53001AD72D /* List */ = { + isa = PBXGroup; + children = ( + C1935A1F2C89CA5A001AD72D /* Table */, + ); + name = List; + sourceTree = ""; + }; + C1935A1F2C89CA5A001AD72D /* Table */ = { + isa = PBXGroup; + children = ( + C1935A202C89CA5F001AD72D /* Survey */, + C1935A232C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift */, + ); + name = Table; + sourceTree = ""; + }; + C1935A202C89CA5F001AD72D /* Survey */ = { + isa = PBXGroup; + children = ( + C1935A212C89CA9F001AD72D /* AutofillSurveyManagerTests.swift */, + ); + name = Survey; + sourceTree = ""; + }; C1AFFC4B2B8773060060448E /* AuthConfirmation */ = { isa = PBXGroup; children = ( @@ -6162,9 +6217,9 @@ F40F843228C92B1C0081AE75 /* Autofill */ = { isa = PBXGroup; children = ( + C1935A1D2C89CA4B001AD72D /* Management */, C185ED622BD4388F00BAE9DC /* Import */, C1BF0BA629B63E0400482B73 /* AutofillLoginUI */, - F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */, C1D21E2E293A599C006E5A05 /* AutofillLoginSessionTests.swift */, C1CDA31D2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift */, ); @@ -7295,6 +7350,7 @@ 1E8AD1C927BFAD1500ABA377 /* DirectoryMonitor.swift in Sources */, 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, 1E8AD1D127C000AB00ABA377 /* OngoingDownloadRow.swift in Sources */, + C1935A0E2C88D11D001AD72D /* AutofillSurveyView.swift in Sources */, 1DEAADF02BA46E0700E25A97 /* PrivateSearchView.swift in Sources */, 85058366219AE9EA00ED4EDB /* HomePageConfiguration.swift in Sources */, 1E9529A12C4E748B006E80D4 /* UINavigationControllerExtension.swift in Sources */, @@ -7315,6 +7371,7 @@ C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, 85B9CB8921AEBDD5009001F1 /* FavoriteHomeCell.swift in Sources */, + C1935A102C88D131001AD72D /* AutofillSurveyManager.swift in Sources */, 98999D5922FDA41500CBBE1B /* BasicAuthenticationAlert.swift in Sources */, C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */, 1DDF40202BA049FA006850D9 /* SettingsRootView.swift in Sources */, @@ -7370,6 +7427,7 @@ F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */, 85449EF523FDA02800512AAF /* KeyboardSettingsViewController.swift in Sources */, 85C11E4C2090888C00BFFEB4 /* HomeRowReminder.swift in Sources */, + C1935A122C88D1D8001AD72D /* AutofillHeaderViewFactory.swift in Sources */, 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, @@ -7749,6 +7807,7 @@ 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */, 98AAF8E4292EB46000DBDF06 /* BookmarksMigrationTests.swift in Sources */, 85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */, + C1935A242C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift in Sources */, C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */, 6F7FB8E72C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift in Sources */, 851CD674244D7E6000331B98 /* UserDefaultsExtension.swift in Sources */, @@ -7829,6 +7888,7 @@ CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */, 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */, 310742AB2848E6FD0012660B /* BackForwardMenuHistoryItemURLSanitizerTests.swift in Sources */, + C1935A222C89CA9F001AD72D /* AutofillSurveyManagerTests.swift in Sources */, 22CB1ED8203DDD2C00D2C724 /* AppDeepLinksTests.swift in Sources */, 9847C00527A41A0A00DB07AA /* WebViewTestHelper.swift in Sources */, 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json new file mode 100644 index 0000000000..608aeb0457 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Passwords-DDG-96x96.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg new file mode 100644 index 0000000000..2c92f4ff23 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/AutofillDebugViewController.swift b/DuckDuckGo/AutofillDebugViewController.swift index 78ebb76778..dad15dbfa9 100644 --- a/DuckDuckGo/AutofillDebugViewController.swift +++ b/DuckDuckGo/AutofillDebugViewController.swift @@ -33,6 +33,7 @@ class AutofillDebugViewController: UITableViewController { case resetAutofillData = 204 case addAutofillData = 205 case resetAutofillBrokenReports = 206 + case resetAutofillSurveys = 207 } let defaults = AppUserDefaults() @@ -87,6 +88,11 @@ class AutofillDebugViewController: UITableViewController { let expiryDate = Calendar.current.date(byAdding: .day, value: 60, to: Date())! _ = reporter.persistencyManager.removeExpiredItems(currentDate: expiryDate) ActionMessageView.present(message: "Autofill Broken Reports reset") + } else if cell.tag == Row.resetAutofillSurveys.rawValue { + tableView.deselectRow(at: indexPath, animated: true) + let autofillSurveyManager = AutofillSurveyManager() + autofillSurveyManager.resetSurveys() + ActionMessageView.present(message: "Autofill Surveys reset") } } } @@ -114,7 +120,7 @@ class AutofillDebugViewController: UITableViewController { let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) for i in 1...count { - let account = SecureVaultModels.WebsiteAccount(title: "", username: "Dax \(i)", domain: "https://fill.dev", notes: "") + let account = SecureVaultModels.WebsiteAccount(title: "", username: "Dax \(i)", domain: "fill.dev", notes: "") let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)) do { _ = try secureVault?.storeWebsiteCredentials(credentials) diff --git a/DuckDuckGo/AutofillHeaderViewFactory.swift b/DuckDuckGo/AutofillHeaderViewFactory.swift new file mode 100644 index 0000000000..dd5eb7322e --- /dev/null +++ b/DuckDuckGo/AutofillHeaderViewFactory.swift @@ -0,0 +1,92 @@ +// +// AutofillHeaderViewFactory.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 UIKit +import SwiftUI +import Core + +protocol AutofillHeaderViewDelegate: AnyObject { + func handlePrimaryAction(for headerType: AutofillHeaderViewFactory.ViewType) + func handleDismissAction(for headerType: AutofillHeaderViewFactory.ViewType) +} + +protocol AutofillHeaderViewFactoryProtocol: AnyObject { + var delegate: AutofillHeaderViewDelegate? { get set } + + func makeHeaderView(for type: AutofillHeaderViewFactory.ViewType) -> UIViewController +} + +final class AutofillHeaderViewFactory: AutofillHeaderViewFactoryProtocol { + + weak var delegate: AutofillHeaderViewDelegate? + + enum ViewType { + case syncPromo(SyncPromoManager.Touchpoint) + case survey(AutofillSurveyManager.AutofillSurvey) + } + + init(delegate: AutofillHeaderViewDelegate?) { + self.delegate = delegate + } + + func makeHeaderView(for type: ViewType) -> UIViewController { + switch type { + case .syncPromo(let touchpointType): + return makeSyncPromoView(touchpointType: touchpointType) + case .survey(let survey): + return makeSurveyView(survey: survey) + } + } + + private func makeSyncPromoView(touchpointType: SyncPromoManager.Touchpoint) -> UIHostingController { + let headerView = SyncPromoView(viewModel: SyncPromoViewModel( + touchpointType: touchpointType, + primaryButtonAction: { [weak delegate] in + delegate?.handlePrimaryAction(for: .syncPromo(touchpointType)) + }, + dismissButtonAction: { [weak delegate] in + delegate?.handleDismissAction(for: .syncPromo(touchpointType)) + } + )) + + Pixel.fire(.syncPromoDisplayed, withAdditionalParameters: ["source": touchpointType.rawValue]) + + let hostingController = UIHostingController(rootView: headerView) + hostingController.view.backgroundColor = .clear + return hostingController + } + + private func makeSurveyView(survey: AutofillSurveyManager.AutofillSurvey) -> UIHostingController { + let headerView = AutofillSurveyView( + primaryButtonAction: { [weak delegate] in + delegate?.handlePrimaryAction(for: .survey(survey)) + }, + dismissButtonAction: { [weak delegate] in + delegate?.handleDismissAction(for: .survey(survey)) + } + ) + + Pixel.fire(pixel: .autofillManagementScreenVisitSurveyAvailable) + + let hostingController = UIHostingController(rootView: headerView) + hostingController.view.backgroundColor = .clear + return hostingController + } +} diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 77ac3f4bb1..5c2ff7a615 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -95,6 +95,7 @@ final class AutofillLoginListViewModel: ObservableObject { private let autofillDomainNameUrlMatcher = AutofillDomainNameUrlMatcher() private let autofillDomainNameUrlSort = AutofillDomainNameUrlSort() private let syncService: DDGSyncing + private let locale: Locale private var showBreakageReporter: Bool = false private lazy var reporterDateFormatter = { @@ -110,6 +111,8 @@ final class AutofillLoginListViewModel: ObservableObject { private lazy var syncPromoManager: SyncPromoManaging = SyncPromoManager(syncService: syncService) + private lazy var autofillSurveyManager: AutofillSurveyManaging = AutofillSurveyManager() + internal lazy var breakageReporter = BrokenSiteReporter(pixelHandler: { [weak self] _ in if let currentTabUid = self?.currentTabUid { NotificationCenter.default.post(name: .autofillFailureReport, object: self, userInfo: [UserInfoKeys.tabUid: currentTabUid]) @@ -118,7 +121,7 @@ final class AutofillLoginListViewModel: ObservableObject { self?.showBreakageReporter = false }, keyValueStoring: keyValueStore, storageConfiguration: .autofillConfig) - @Published private (set) var viewState: AutofillLoginListViewModel.ViewState = .authLocked + @Published private(set) var viewState: AutofillLoginListViewModel.ViewState = .authLocked @Published private(set) var sections = [AutofillLoginListSectionType]() { didSet { updateViewState() @@ -156,7 +159,8 @@ final class AutofillLoginListViewModel: ObservableObject { autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager = AppDependencyProvider.shared.autofillNeverPromptWebsitesManager, privacyConfig: PrivacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig, keyValueStore: KeyValueStoringDictionaryRepresentable = UserDefaults.standard, - syncService: DDGSyncing) { + syncService: DDGSyncing, + locale: Locale = Locale.current) { self.appSettings = appSettings self.tld = tld self.secureVault = secureVault @@ -166,6 +170,7 @@ final class AutofillLoginListViewModel: ObservableObject { self.privacyConfig = privacyConfig self.keyValueStore = keyValueStore self.syncService = syncService + self.locale = locale if let count = getAccountsCount() { authenticationNotRequired = count == 0 || AppDependencyProvider.shared.autofillLoginSession.isSessionValid @@ -329,6 +334,24 @@ final class AutofillLoginListViewModel: ObservableObject { syncPromoManager.dismissPromoFor(.passwords) } + func getSurveyToPresent() -> AutofillSurveyManager.AutofillSurvey? { + guard locale.isEnglishLanguage, + viewState == .showItems || viewState == .empty, + !isEditing, + privacyConfig.isEnabled(featureKey: .autofillSurveys) else { + return nil + } + return autofillSurveyManager.surveyToPresent(settings: privacyConfig.settings(for: .autofillSurveys)) + } + + func surveyUrl(survey: String) -> URL? { + return autofillSurveyManager.buildSurveyUrl(survey, accountsCount: accountsCount) + } + + func dismissSurvey(id: String) { + autofillSurveyManager.markSurveyAsCompleted(id: id) + } + // MARK: Private Methods private func saveReport(for currentTabUrl: URL) { diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index b3823fd806..65a3c5c59a 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -149,21 +149,11 @@ final class AutofillLoginSettingsListViewController: UIViewController { return tableView }() - private lazy var syncPromoViewHostingController: UIHostingController = { - let headerView = SyncPromoView(viewModel: SyncPromoViewModel(touchpointType: .passwords, primaryButtonAction: { [weak self] in - self?.segueToSync(source: "promotion_passwords") - Pixel.fire(.syncPromoConfirmed, withAdditionalParameters: ["source": SyncPromoManager.Touchpoint.passwords.rawValue]) - }, dismissButtonAction: { [weak self] in - self?.viewModel.dismissSyncPromo() - self?.updateTableHeaderView() - })) - - Pixel.fire(.syncPromoDisplayed, withAdditionalParameters: ["source": SyncPromoManager.Touchpoint.passwords.rawValue]) - - let hostingController = UIHostingController(rootView: headerView) - hostingController.view.backgroundColor = .clear - return hostingController - }() + private lazy var headerViewFactory: AutofillHeaderViewFactoryProtocol = AutofillHeaderViewFactory(delegate: self) + private var currentHeaderHostingController: UIViewController? + + // This is used to prevent the Sync Promo from being displayed immediately after the Survey is dismissed + private var surveyPromptPresented: Bool = false private lazy var lockedViewBottomConstraint: NSLayoutConstraint = { NSLayoutConstraint(item: tableView, @@ -672,22 +662,67 @@ final class AutofillLoginSettingsListViewController: UIViewController { } private func updateTableHeaderView() { - if viewModel.shouldShowSyncPromo() { - guard tableView.frame != .zero, tableView.tableHeaderView != syncPromoViewHostingController.view else { - return + guard tableView.frame != .zero else { + return + } + + if let survey = viewModel.getSurveyToPresent() { + if shouldUpdateHeaderView(for: .survey(survey)) { + configureTableHeaderView(for: .survey(survey)) + surveyPromptPresented = true + } + return + } + + if viewModel.shouldShowSyncPromo() && !surveyPromptPresented { + if shouldUpdateHeaderView(for: .syncPromo(.passwords)) { + configureTableHeaderView(for: .syncPromo(.passwords)) } + return + } - addChild(syncPromoViewHostingController) + // No header view is needed, clear the table header + clearTableHeaderView() + } - let syncPromoViewHeight = syncPromoViewHostingController.view.sizeThatFits(CGSize(width: tableView.bounds.width - tableView.layoutMargins.left - tableView.layoutMargins.right, height: CGFloat.greatestFiniteMagnitude)).height - syncPromoViewHostingController.view.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: syncPromoViewHeight) - tableView.tableHeaderView = syncPromoViewHostingController.view + private func shouldUpdateHeaderView(for type: AutofillHeaderViewFactory.ViewType) -> Bool { + if let currentHeaderView = tableView.tableHeaderView, + let headerView = currentHeaderHostingController?.view, + currentHeaderView == headerView { + return false + } + return true + } - syncPromoViewHostingController.didMove(toParent: self) - } else { - guard tableView.tableHeaderView != nil else { - return + private func configureTableHeaderView(for type: AutofillHeaderViewFactory.ViewType) { + switch type { + case .survey(let survey): + currentHeaderHostingController = headerViewFactory.makeHeaderView(for: .survey(survey)) + if let hostingController = currentHeaderHostingController as? UIHostingController { + setupTableHeaderView(with: hostingController) + } + case .syncPromo(let promoType): + currentHeaderHostingController = headerViewFactory.makeHeaderView(for: .syncPromo(promoType)) + if let hostingController = currentHeaderHostingController as? UIHostingController { + setupTableHeaderView(with: hostingController) } + } + } + + private func setupTableHeaderView(with hostingController: UIViewController) { + addChild(hostingController) + + let viewWidth = tableView.bounds.width - tableView.layoutMargins.left - tableView.layoutMargins.right + let viewHeight = hostingController.view.sizeThatFits(CGSize(width: viewWidth, height: CGFloat.greatestFiniteMagnitude)).height + + hostingController.view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) + tableView.tableHeaderView = hostingController.view + + hostingController.didMove(toParent: self) + } + + private func clearTableHeaderView() { + if tableView.tableHeaderView != nil { tableView.tableHeaderView = nil } } @@ -1115,6 +1150,38 @@ extension AutofillLoginSettingsListViewController { } } +// MARK: AutofillHeaderViewDelegate + +extension AutofillLoginSettingsListViewController: AutofillHeaderViewDelegate { + + func handlePrimaryAction(for headerType: AutofillHeaderViewFactory.ViewType) { + switch headerType { + case .survey(let survey): + if let surveyURL = viewModel.surveyUrl(survey: survey.url) { + LaunchTabNotification.postLaunchTabNotification(urlString: surveyURL.absoluteString) + self.dismiss(animated: true) + } + viewModel.dismissSurvey(id: survey.id) + case .syncPromo(let touchpoint): + segueToSync(source: "promotion_passwords") + Pixel.fire(.syncPromoConfirmed, withAdditionalParameters: ["source": touchpoint.rawValue]) + } + } + + func handleDismissAction(for headerType: AutofillHeaderViewFactory.ViewType) { + defer { + updateTableHeaderView() + } + + switch headerType { + case .survey(let survey): + viewModel.dismissSurvey(id: survey.id) + case .syncPromo: + viewModel.dismissSyncPromo() + } + } +} + extension NSNotification.Name { static let autofillFailureReport: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.notification.autofillFailureReport") } diff --git a/DuckDuckGo/AutofillSurveyManager.swift b/DuckDuckGo/AutofillSurveyManager.swift new file mode 100644 index 0000000000..c8b2d2581b --- /dev/null +++ b/DuckDuckGo/AutofillSurveyManager.swift @@ -0,0 +1,126 @@ +// +// AutofillSurveyManager.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 +import Core +import RemoteMessaging + +protocol AutofillSurveyManaging { + func surveyToPresent(settings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings) -> AutofillSurveyManager.AutofillSurvey? + func markSurveyAsCompleted(id: String) + func resetSurveys() + func buildSurveyUrl(_ url: String, accountsCount: Int) -> URL? +} + +final class AutofillSurveyManager: AutofillSurveyManaging { + + struct AutofillSurvey { + let id: String + let url: String + } + + @UserDefaultsWrapper(key: .autofillSurveysCompleted, defaultValue: []) + private var autofillSurveysCompleted: [String] + + private enum BucketName: String { + case none + case few + case some + case many + case lots + } + + private enum Constants { + static let surveysSettingsKey = "surveys" + static let surveysIdSettingsKey = "id" + static let surveysUrlSettingsKey = "url" + static let savedPasswordsQueryParam = "saved_passwords" + static let listQueryParam = "list" + } + + func surveyToPresent(settings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings) -> AutofillSurvey? { + guard let surveys = settings[Constants.surveysSettingsKey] as? [[String: Any]] else { + return nil + } + + for survey in surveys { + guard let id = survey[Constants.surveysIdSettingsKey] as? String, + let url = survey[Constants.surveysUrlSettingsKey] as? String, + !hasCompletedSurvey(id: id) else { + continue + } + return AutofillSurvey(id: id, url: url) + } + + return nil + } + + func markSurveyAsCompleted(id: String) { + autofillSurveysCompleted.append(id) + } + + func resetSurveys() { + autofillSurveysCompleted.removeAll() + } + + func buildSurveyUrl(_ url: String, accountsCount: Int) -> URL? { + guard let surveyURL = URL(string: url) else { + return nil + } + + let surveyURLBuilder = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: StatisticsUserDefaults(), + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) + let url = surveyURLBuilder.add(parameters: [.appVersion, .atb, .atbVariant, .daysInstalled, .hardwareModel, .osVersion, .vpnFirstUsed, .vpnLastUsed], to: surveyURL) + return addPasswordsCountSurveyParameter(to: url, accountsCount: accountsCount) + } + + private func hasCompletedSurvey(id: String) -> Bool { + autofillSurveysCompleted.contains(id) + } + + private func addPasswordsCountSurveyParameter(to surveyURL: URL, accountsCount: Int) -> URL { + guard var components = URLComponents(string: surveyURL.absoluteString) else { + assertionFailure("Could not build URL components from survey URL") + return surveyURL + } + + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: Constants.savedPasswordsQueryParam, + value: bucketNameFrom(count: accountsCount))) + components.queryItems = queryItems + + return components.url ?? surveyURL + } + + private func bucketNameFrom(count: Int) -> String { + if count == 0 { + return BucketName.none.rawValue + } else if count < 4 { + return BucketName.few.rawValue + } else if count < 11 { + return BucketName.some.rawValue + } else if count < 50 { + return BucketName.many.rawValue + } else { + return BucketName.lots.rawValue + } + } +} diff --git a/DuckDuckGo/AutofillSurveyView.swift b/DuckDuckGo/AutofillSurveyView.swift new file mode 100644 index 0000000000..d61bc661e1 --- /dev/null +++ b/DuckDuckGo/AutofillSurveyView.swift @@ -0,0 +1,100 @@ +// +// AutofillSurveyView.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 Core +import DesignResourcesKit +import DuckUI +import SwiftUI + +struct AutofillSurveyView: View { + var primaryButtonAction: (() -> Void)? + var dismissButtonAction: (() -> Void)? + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(spacing: 8) { + Group { + Image(.passwordsDDG96X96) + .resizable() + .frame(width: 50, height: 50, alignment: .center) + + Text(verbatim: "Help us improve!") + .daxHeadline() + .foregroundColor(Color(designSystemColor: .textPrimary)) + .padding(.top, 8) + .frame(maxWidth: .infinity) + + Text(verbatim: "We want to make using passwords in DuckDuckGo better.") + .daxBodyRegular() + .foregroundColor(Color(designSystemColor: .textPrimary)) + .padding(.top, 4) + + Button { + primaryButtonAction?() + } label: { + HStack { + Text(verbatim: "Take Survey") + .daxButton() + } + } + .buttonStyle(PrimaryButtonStyle(compact: true, fullWidth: false)) + .padding(.top, 8) + } + .padding(.horizontal, 24) + } + .multilineTextAlignment(.center) + .padding(.vertical) + .padding(.horizontal, 8) + + VStack { + HStack { + Spacer() + Button { + dismissButtonAction?() + } label: { + Image(.close24) + .foregroundColor(.primary) + } + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + .padding(0) + } + } + .alignmentGuide(.top) { dimension in + dimension[.top] + } + } + .background(RoundedRectangle(cornerRadius: 8.0) + .foregroundColor(Color(designSystemColor: .surface)) + ) + .padding([.horizontal, .top], 20) + .padding(.bottom, 30) + } + +} + +#Preview("Light") { + AutofillSurveyView() + .preferredColorScheme(.light) +} + +#Preview("Dark") { + AutofillSurveyView() + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index c38326876d..bc27b68634 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -491,12 +491,21 @@ + + + + + + + + + - + @@ -505,7 +514,7 @@ - + diff --git a/DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift b/DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift new file mode 100644 index 0000000000..b2f67e28b8 --- /dev/null +++ b/DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift @@ -0,0 +1,144 @@ +// +// AutofillHeaderViewFactoryTests.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 SwiftUI +@testable import DuckDuckGo + +class MockAutofillHeaderViewDelegate: AutofillHeaderViewDelegate { + var didHandlePrimaryAction = false + var didHandleDismissAction = false + var lastHandledHeaderType: AutofillHeaderViewFactory.ViewType? + + func handlePrimaryAction(for headerType: AutofillHeaderViewFactory.ViewType) { + didHandlePrimaryAction = true + lastHandledHeaderType = headerType + } + + func handleDismissAction(for headerType: AutofillHeaderViewFactory.ViewType) { + didHandleDismissAction = true + lastHandledHeaderType = headerType + } +} + +final class AutofillHeaderViewFactoryTests: XCTestCase { + + var factory: AutofillHeaderViewFactory! + var mockDelegate: MockAutofillHeaderViewDelegate! + + override func setUpWithError() throws { + try super.setUpWithError() + + mockDelegate = MockAutofillHeaderViewDelegate() + factory = AutofillHeaderViewFactory(delegate: mockDelegate) + } + + override func tearDownWithError() throws { + factory = nil + mockDelegate = nil + + try super.tearDownWithError() + } + + func testWhenMakeHeaderViewForSyncPromoThenSyncPromoViewIsReturned() { + let viewController = factory.makeHeaderView(for: .syncPromo(.passwords)) + XCTAssertTrue(viewController is UIHostingController) + if let hostingController = viewController as? UIHostingController { + XCTAssertNotNil(hostingController.rootView) + } + } + + func testWhenMakeHeaderViewForSurveyThenAutofillSurveyViewIsReturned() { + let survey = AutofillSurveyManager.AutofillSurvey(id: "testSurvey", url: "https://example.com") + let viewController = factory.makeHeaderView(for: .survey(survey)) + + XCTAssertTrue(viewController is UIHostingController) + if let hostingController = viewController as? UIHostingController { + XCTAssertNotNil(hostingController.rootView) + } + } + + func testWhenSyncPromoPrimaryButtonActionIsCalledThenDelegateHandlePrimaryActionIsCalled() throws { + let touchpoint = SyncPromoManager.Touchpoint.passwords + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .syncPromo(touchpoint)) as? UIHostingController, "Expected a UIHostingController") + + let primaryButtonAction = try XCTUnwrap(viewController.rootView.viewModel.primaryButtonAction, "Primary button action should not be nil") + primaryButtonAction() + + XCTAssertTrue(mockDelegate.didHandlePrimaryAction) + if case .syncPromo(let receivedTouchpoint) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedTouchpoint, touchpoint) + } else { + XCTFail("Expected .syncPromo ViewType with touchpoint \(touchpoint)") + } + } + + func testWhenSyncPromoDismissButtonActionIsCalledThenDelegateHandleDismissActionIsCalled() throws { + let touchpoint = SyncPromoManager.Touchpoint.passwords + + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .syncPromo(touchpoint)) as? UIHostingController, "Expected a UIHostingController") + + let dismissButtonAction = try XCTUnwrap(viewController.rootView.viewModel.dismissButtonAction, "Dismiss button action should not be nil") + dismissButtonAction() + + XCTAssertTrue(mockDelegate.didHandleDismissAction) + + if case .syncPromo(let receivedTouchpoint) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedTouchpoint, touchpoint) + } else { + XCTFail("Expected .syncPromo ViewType with touchpoint \(touchpoint)") + } + } + + func testWhenSurveyPrimaryButtonActionIsCalledThenDelegateHandlePrimaryActionIsCalled() throws { + let survey = AutofillSurveyManager.AutofillSurvey(id: "testSurvey", url: "https://example.com") + + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .survey(survey)) as? UIHostingController, "Expected a UIHostingController") + + let primaryButtonAction = try XCTUnwrap(viewController.rootView.primaryButtonAction, "Primary button action should not be nil") + primaryButtonAction() + + XCTAssertTrue(mockDelegate.didHandlePrimaryAction) + + if case .survey(let receivedSurvey) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedSurvey.id, survey.id) + XCTAssertEqual(receivedSurvey.url, survey.url) + } else { + XCTFail("Expected .survey ViewType with survey \(survey)") + } + } + + func testWhenSurveyDismissButtonActionIsCalledThenDelegateHandleDismissActionIsCalled() throws { + let survey = AutofillSurveyManager.AutofillSurvey(id: "testSurvey", url: "https://example.com") + + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .survey(survey)) as? UIHostingController, "Expected a UIHostingController") + + let dismissButtonAction = try XCTUnwrap(viewController.rootView.dismissButtonAction, "Dismiss button action should not be nil") + dismissButtonAction() + + XCTAssertTrue(mockDelegate.didHandleDismissAction) + + if case .survey(let receivedSurvey) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedSurvey.id, survey.id) + XCTAssertEqual(receivedSurvey.url, survey.url) + } else { + XCTFail("Expected .survey ViewType with survey \(survey)") + } + } +} diff --git a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift index f4f0880d8e..0ff5c5ac3a 100644 --- a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift +++ b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift @@ -50,6 +50,17 @@ class AutofillLoginListViewModelTests: XCTestCase { } ] }, + "autofillSurveys": { + "state": "enabled", + "settings": { + "surveys": [ + { + "id": "123", + "url": "https://asurveyurl.com" + } + ] + }, + }, }, "unprotectedTemporary": [] } @@ -65,6 +76,17 @@ class AutofillLoginListViewModelTests: XCTestCase { }, "exceptions": [] }, + "autofillSurveys": { + "state": "disabled", + "settings": { + "surveys": [ + { + "id": "240900", + "url": "https://asurveyurl.com" + } + ] + }, + }, }, "unprotectedTemporary": [] } @@ -72,6 +94,7 @@ class AutofillLoginListViewModelTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() + setupUserDefault(with: #file) manager = AutofillNeverPromptWebsitesManager(secureVault: vault) syncService = MockDDGSyncing(authState: .inactive, scheduler: CapturingScheduler(), isSyncInProgress: false) } @@ -492,7 +515,7 @@ class AutofillLoginListViewModelTests: XCTestCase { let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, - currentTabUrl: URL(string: "https://\(testDomain)"), + currentTabUrl: currentTabUrl, currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), @@ -529,6 +552,60 @@ class AutofillLoginListViewModelTests: XCTestCase { XCTAssertTrue(model.shouldShowBreakageReporter()) } + + func testWhenLocaleIsNotEnglishThenNoSurveyIsReturned() { + let nonEnglishLocale = Locale(identifier: "es") + let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, autofillNeverPromptWebsitesManager: manager, syncService: syncService, locale: nonEnglishLocale) + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenViewStateIsIneligibleThenNoSurveyIsReturned() throws { + vault.storedAccounts = [ + SecureVaultModels.WebsiteAccount(id: "1", title: nil, username: "", domain: "testsite.com", created: Date(), lastUpdated: Date()), + SecureVaultModels.WebsiteAccount(id: "2", title: nil, username: "", domain: "testsite.com", created: Date(), lastUpdated: Date()) + ] + for account in vault.storedAccounts { + _ = try vault.storeWebsiteCredentials(SecureVaultModels.WebsiteCredentials(account: account, password: nil)) + } + let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, autofillNeverPromptWebsitesManager: manager, syncService: syncService) + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenIsEditingThenNoSurveyIsReturned() { + let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, autofillNeverPromptWebsitesManager: manager, syncService: syncService) + model.isEditing = true + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenSurveyConfigIsDisabledThenNoSurveyIsReturned() { + let model = AutofillLoginListViewModel(appSettings: appSettings, + tld: tld, + secureVault: vault, + privacyConfig: makePrivacyConfig(from: configDisabled), + syncService: syncService) + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenAllConditionsAreMetThenSurveyIsReturnedAndWhenDismissedNotSurveyIsReturned() { + let model = AutofillLoginListViewModel(appSettings: appSettings, + tld: tld, + secureVault: vault, + privacyConfig: makePrivacyConfig(from: configEnabled), + syncService: syncService) + let survey = model.getSurveyToPresent() + XCTAssertNotNil(survey) + XCTAssertEqual(survey?.id, "123") + XCTAssertEqual(survey?.url, "https://asurveyurl.com") + + model.dismissSurvey(id: "123") + + XCTAssertNil(model.getSurveyToPresent()) + } + } class AutofillLoginListSectionTypeTests: XCTestCase { diff --git a/DuckDuckGoTests/AutofillSurveyManagerTests.swift b/DuckDuckGoTests/AutofillSurveyManagerTests.swift new file mode 100644 index 0000000000..61f8e350b7 --- /dev/null +++ b/DuckDuckGoTests/AutofillSurveyManagerTests.swift @@ -0,0 +1,118 @@ +// +// AutofillSurveyManagerTests.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 DuckDuckGo + +final class AutofillSurveyManagerTests: XCTestCase { + + private var manager: AutofillSurveyManager! + + override func setUpWithError() throws { + try super.setUpWithError() + + setupUserDefault(with: #file) + manager = AutofillSurveyManager() + manager.resetSurveys() + } + + override func tearDownWithError() throws { + manager.resetSurveys() + manager = nil + + try super.tearDownWithError() + } + + func testSurveyToPresentReturnsCorrectSurvey() { + let settings: [String: Any] = [ + "surveys": [ + ["id": "1", "url": "https://example.com/survey1"], + ["id": "2", "url": "https://example.com/survey2"] + ] + ] + + let survey = manager.surveyToPresent(settings: settings as PrivacyConfigurationData.PrivacyFeature.FeatureSettings) + XCTAssertNotNil(survey) + XCTAssertEqual(survey?.id, "1") + XCTAssertEqual(survey?.url, "https://example.com/survey1") + } + + func testSurveyToPresentSkipsCompletedSurveys() { + let settings: [String: Any] = [ + "surveys": [ + ["id": "1", "url": "https://example.com/survey1"], + ["id": "2", "url": "https://example.com/survey2"] + ] + ] + + manager.markSurveyAsCompleted(id: "1") + + let survey = manager.surveyToPresent(settings: settings as PrivacyConfigurationData.PrivacyFeature.FeatureSettings) + XCTAssertNotNil(survey) + XCTAssertEqual(survey?.id, "2") + XCTAssertEqual(survey?.url, "https://example.com/survey2") + } + + func testBuildSurveyUrlValid() { + let url = "https://example.com/survey" + let accountsCount = 5 + let resultUrl = manager.buildSurveyUrl(url, accountsCount: accountsCount) + XCTAssertNotNil(resultUrl) + XCTAssertEqual(resultUrl?.host, "example.com") + XCTAssertTrue(resultUrl?.query?.contains("saved_passwords=some") ?? false) + } + + func testAddPasswordsCountSurveyParameter() { + let baseURL = URL(string: "https://example.com/survey")! + let modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 10) + XCTAssertNotNil(modifiedURL) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=some"), true) + } + + func testPasswordsCountHasCorrectBucketNameSurveyParameter() { + let baseURL = URL(string: "https://example.com/survey")! + var modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 0) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=none"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 1) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=few"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 3) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=few"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 4) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=some"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 10) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=some"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 11) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=many"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 49) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=many"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 50) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=lots"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 100) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=lots"), true) + } +} From 4ed55a45111f3f075c3f7111a62dace15ac878e9 Mon Sep 17 00:00:00 2001 From: Thom Espach Date: Tue, 10 Sep 2024 12:00:08 +0100 Subject: [PATCH 09/31] Phishing Detection Library Implementation (#3336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1208253815417552/f Tech Design URL: CC: **Description**: iOS integration for BSK to implement phishing detection on macOS. macOS PR: https://github.com/duckduckgo/BrowserServicesKit/pull/935 BSK PR: https://github.com/duckduckgo/macos-browser/pull/3206 **Steps to test this PR**: 1. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/FeatureFlag.swift | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index ce78a78286..229189a0a1 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -80,7 +80,7 @@ extension FeatureFlag: FeatureFlagSourceProviding { case .duckPlayer: return .remoteReleasable(.feature(.duckPlayer)) case .sslCertificatesBypass: - return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) + return .remoteReleasable(.subfeature(SslCertificatesSubfeature.allowBypass)) case .syncPromotionBookmarks: return .remoteReleasable(.subfeature(SyncPromotionSubfeature.bookmarks)) case .syncPromotionPasswords: diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index db6bfd8eaa..620e8fd3d5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10713,7 +10713,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 191.2.3; + version = 192.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 81fa28071f..9a392633c7 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" : "b83ccf14b5844e8876de04bcc3074a15569f3b26", - "version" : "191.2.3" + "revision" : "3dc2650ab4a88e471e9a194360f75b66ffbce9bd", + "version" : "192.0.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "665b23dc656c9f787494620494f8e56098a900b2", - "version" : "5.1.1" + "revision" : "9de2b2aa317a48d3ee31116dc15b0feeb2cc9414", + "version" : "5.3.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" From af0293491dd78b7ac8879ec3f4caf94f405b8cf1 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Tue, 10 Sep 2024 12:04:53 +0100 Subject: [PATCH 10/31] Fix image cache VC segue (#3338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414709148257752/1208263529112586/f Tech Design URL: CC: **Description**: As title, it somehow disappeared. **Steps to test this PR**: 1. Check you can see the image cache in Settings -> Debug **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo/Debug.storyboard | 19 +++++++++++-------- DuckDuckGo/RootDebugViewController.swift | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index c38326876d..6194cf92dc 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -55,6 +55,9 @@ + + + @@ -357,7 +360,7 @@ - + @@ -962,17 +965,17 @@ - + - + - + - + diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index 6f861725a0..287ef73822 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -91,7 +91,7 @@ class RootDebugViewController: UITableViewController { super.init(coder: coder) } - @IBSegueAction func onCreateImageCacheDebugScreen(_ coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ImageCacheDebugViewController { + @IBSegueAction func onCreateImageCacheDebugScreen(_ coder: NSCoder) -> ImageCacheDebugViewController? { guard let controller = ImageCacheDebugViewController(coder: coder, bookmarksDatabase: self.bookmarksDatabase!) else { fatalError("Failed to create controller") From f4077f8fc5468323dad1ff8e430f7aa4379d838b Mon Sep 17 00:00:00 2001 From: amddg44 Date: Tue, 10 Sep 2024 13:32:32 +0200 Subject: [PATCH 11/31] Release 7.137.0-1 (#3342) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9a57964978..b690db21d5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8988,7 +8988,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9025,7 +9025,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9115,7 +9115,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9142,7 +9142,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9291,7 +9291,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9316,7 +9316,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9385,7 +9385,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9419,7 +9419,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9452,7 +9452,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9482,7 +9482,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9792,7 +9792,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9823,7 +9823,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9851,7 +9851,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9884,7 +9884,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9914,7 +9914,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9947,11 +9947,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10184,7 +10184,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10211,7 +10211,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10243,7 +10243,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10280,7 +10280,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10315,7 +10315,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10350,11 +10350,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10527,11 +10527,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10560,10 +10560,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 7a6a5c81fab0b9265211f6898b7d8f79b2a8cc66 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 10 Sep 2024 14:52:49 +0200 Subject: [PATCH 12/31] BSK bump for macOS password import promotion flow (#3332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1207617229892922/f **Description**: Nothing iOS related in this one. Just a plain BSK bump. **Steps to test this PR**: - CI check should be fine but could perhaps do a quick smoke test of the autofill initial inputs flow **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +++- .../xcshareddata/swiftpm/Package.resolved | 8 ++--- ...StubAutofillLoginImportStateProvider.swift | 35 +++++++++++++++++++ DuckDuckGo/UserScripts.swift | 2 +- DuckDuckGoTests/WebCacheManagerTests.swift | 4 +-- 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 DuckDuckGo/StubAutofillLoginImportStateProvider.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ad19537f1f..40d5155a9a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -986,6 +986,7 @@ EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */; }; EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */; }; EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EE0D1B9C2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */; }; EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; @@ -2774,6 +2775,7 @@ EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsViewModel.swift; sourceTree = ""; }; EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNLocationView.swift; sourceTree = ""; }; EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; + EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubAutofillLoginImportStateProvider.swift; sourceTree = ""; }; EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationPresenter.swift; sourceTree = ""; }; EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInternalUserStoring.swift; sourceTree = ""; }; EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAlpha.entitlements; sourceTree = ""; }; @@ -6244,6 +6246,7 @@ 31951E9328230D8900CAF535 /* Shared */, F407605428131923006B1E0B /* SaveLogin */, EE5929612C5A8AF40029380B /* AutofillUsageMonitor.swift */, + EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */, ); name = Autofill; sourceTree = ""; @@ -7609,6 +7612,7 @@ 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiter.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, 851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */, + EE0D1B9C2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift in Sources */, 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, @@ -10773,7 +10777,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 192.0.0; + version = 193.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9a392633c7..12da85ef52 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" : "3dc2650ab4a88e471e9a194360f75b66ffbce9bd", - "version" : "192.0.0" + "revision" : "e036123b9e0babf0e7c8ada24d4edb932a2730b3", + "version" : "193.0.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "1f12b78d9bac4a1d9b6bad18dc2ef0593bed34a3", - "version" : "13.0.0" + "revision" : "1fee787458d13f8ed07f9fe81aecd6e59609339e", + "version" : "13.1.0" } }, { diff --git a/DuckDuckGo/StubAutofillLoginImportStateProvider.swift b/DuckDuckGo/StubAutofillLoginImportStateProvider.swift new file mode 100644 index 0000000000..326659f898 --- /dev/null +++ b/DuckDuckGo/StubAutofillLoginImportStateProvider.swift @@ -0,0 +1,35 @@ +// +// StubAutofillLoginImportStateProvider.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 + +struct StubAutofillLoginImportStateProvider: AutofillLoginImportStateProvider { + public var isNewDDGUser: Bool = false + public var hasImportedLogins: Bool = false + var credentialsImportPromptPresentationCount: Int = 0 + + var isAutofillEnabled: Bool { + AppDependencyProvider().appSettings.autofillCredentialsEnabled + } + + func hasNeverPromptWebsitesFor(_ domain: String) -> Bool { + AppDependencyProvider().autofillNeverPromptWebsitesManager.hasNeverPromptWebsitesFor(domain: domain) + } +} diff --git a/DuckDuckGo/UserScripts.swift b/DuckDuckGo/UserScripts.swift index 4b003e0a57..30904c6324 100644 --- a/DuckDuckGo/UserScripts.swift +++ b/DuckDuckGo/UserScripts.swift @@ -56,7 +56,7 @@ final class UserScripts: UserScriptsProvider { init(with sourceProvider: ScriptSourceProviding) { contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig) - autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider) + autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider, loginImportStateProvider: StubAutofillLoginImportStateProvider()) autofillUserScript.sessionKey = sourceProvider.contentScopeProperties.sessionKey loginFormDetectionScript = sourceProvider.loginDetectionEnabled ? LoginFormDetectionUserScript() : nil diff --git a/DuckDuckGoTests/WebCacheManagerTests.swift b/DuckDuckGoTests/WebCacheManagerTests.swift index 2a40a5bea2..ecfaa14457 100644 --- a/DuckDuckGoTests/WebCacheManagerTests.swift +++ b/DuckDuckGoTests/WebCacheManagerTests.swift @@ -201,9 +201,9 @@ class WebCacheManagerTests: XCTestCase { XCTAssertEqual(1, cookieStore.cookies.count) XCTAssertEqual(cookieStore.cookies[0].domain, "www.example.com") } - + @MainActor - func testWhenAccessingObservationsDbThenValidDatabasePoolIsReturned() { + func x_testWhenAccessingObservationsDbThenValidDatabasePoolIsReturned() { let pool = WebCacheManager.shared.getValidDatabasePool() XCTAssertNotNil(pool, "DatabasePool should not be nil") } From 17af939e6678df909fc7f62607f46ab0160d7de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 10 Sep 2024 16:21:32 +0200 Subject: [PATCH 13/31] Fix privacy icon glitch (#3343) Task/Issue URL: https://app.asana.com/0/414235014887631/1208264183723705/f **Description**: Fix privacy icon glitching when loading sites. --------- Co-authored-by: Bartek Waresiak --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/PrivacyIconLogic.swift | 5 +++-- DuckDuckGo/TabViewController.swift | 7 ++++--- DuckDuckGoTests/PrivacyIconLogicTests.swift | 15 ++++++++++++++- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b690db21d5..faab3ce402 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10773,7 +10773,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 191.2.3; + version = "191.2.3-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 81fa28071f..674a98fc2d 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" : "b83ccf14b5844e8876de04bcc3074a15569f3b26", - "version" : "191.2.3" + "revision" : "3bac7c0924eeebd58b8868ef44489fac0fb7ec44", + "version" : "191.2.3-1" } }, { diff --git a/DuckDuckGo/PrivacyIconLogic.swift b/DuckDuckGo/PrivacyIconLogic.swift index 9ae915497f..727d46480e 100644 --- a/DuckDuckGo/PrivacyIconLogic.swift +++ b/DuckDuckGo/PrivacyIconLogic.swift @@ -37,8 +37,9 @@ final class PrivacyIconLogic { } else { let config = ContentBlocking.shared.privacyConfigurationManager.privacyConfig let isUserUnprotected = config.isUserUnprotected(domain: privacyInfo.url.host) - - let notFullyProtected = !privacyInfo.https || isUserUnprotected || privacyInfo.serverTrust == nil + + let isServerTrustInvalid = (privacyInfo.shouldCheckServerTrust ? privacyInfo.serverTrust == nil : false) + let notFullyProtected = !privacyInfo.https || isUserUnprotected || isServerTrustInvalid return notFullyProtected ? .shieldWithDot : .shield } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index f413f40730..514b8931de 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -993,7 +993,7 @@ class TabViewController: UIViewController { self.privacyInfo = privacyInfo didGoBackForward = false } else { - privacyInfo = makePrivacyInfo(url: url) + privacyInfo = makePrivacyInfo(url: url, shouldCheckServerTrust: true) } } else { privacyInfo = nil @@ -1001,14 +1001,15 @@ class TabViewController: UIViewController { onPrivacyInfoChanged() } - public func makePrivacyInfo(url: URL) -> PrivacyInfo? { + public func makePrivacyInfo(url: URL, shouldCheckServerTrust: Bool = false) -> PrivacyInfo? { guard let host = url.host else { return nil } let entity = ContentBlocking.shared.trackerDataManager.trackerData.findParentEntityOrFallback(forHost: host) let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, - protectionStatus: makeProtectionStatus(for: host)) + protectionStatus: makeProtectionStatus(for: host), + shouldCheckServerTrust: shouldCheckServerTrust) let isValid = certificateTrustEvaluator.evaluateCertificateTrust(trust: webView.serverTrust) if let isValid { privacyInfo.serverTrust = isValid ? webView.serverTrust : nil diff --git a/DuckDuckGoTests/PrivacyIconLogicTests.swift b/DuckDuckGoTests/PrivacyIconLogicTests.swift index ebecb5d55c..b12ef295e8 100644 --- a/DuckDuckGoTests/PrivacyIconLogicTests.swift +++ b/DuckDuckGoTests/PrivacyIconLogicTests.swift @@ -96,7 +96,7 @@ class PrivacyIconLogicTests: XCTestCase { let url = PrivacyIconLogicTests.pageURL let entity = Entity(displayName: "E", domains: [], prevalence: 100.0) let protectionStatus = ProtectionStatus(unprotectedTemporary: false, enabledFeatures: [], allowlisted: false, denylisted: false) - let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: protectionStatus) + let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: protectionStatus, shouldCheckServerTrust: true) let icon = PrivacyIconLogic.privacyIcon(for: privacyInfo) @@ -105,6 +105,19 @@ class PrivacyIconLogicTests: XCTestCase { XCTAssertEqual(icon, .shieldWithDot) } + func testPrivacyIconIsShieldWithoutDotForNoSecTrustAndShouldCheckServerTrustIsFalse() { + let url = PrivacyIconLogicTests.pageURL + let entity = Entity(displayName: "E", domains: [], prevalence: 100.0) + let protectionStatus = ProtectionStatus(unprotectedTemporary: false, enabledFeatures: [], allowlisted: false, denylisted: false) + let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: protectionStatus) + + let icon = PrivacyIconLogic.privacyIcon(for: privacyInfo) + + XCTAssertTrue(url.isHttps) + XCTAssertTrue(privacyInfo.https) + XCTAssertEqual(icon, .shield) + } + } final class MockSecTrust: SecurityTrust {} From 7750271ac8c1bc0e09c0e828449be4eaa876cd07 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Tue, 10 Sep 2024 19:01:41 +0200 Subject: [PATCH 14/31] Release 7.137.0-2 (#3344) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index faab3ce402..dd89a8ddbf 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8988,7 +8988,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9025,7 +9025,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9115,7 +9115,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9142,7 +9142,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9291,7 +9291,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9316,7 +9316,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9385,7 +9385,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9419,7 +9419,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9452,7 +9452,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9482,7 +9482,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9792,7 +9792,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9823,7 +9823,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9851,7 +9851,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9884,7 +9884,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9914,7 +9914,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9947,11 +9947,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10184,7 +10184,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10211,7 +10211,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10243,7 +10243,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10280,7 +10280,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10315,7 +10315,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10350,11 +10350,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10527,11 +10527,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10560,10 +10560,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 6f3e9bb2b46e3acc87d470032935849841bce807 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 10 Sep 2024 16:58:14 -0700 Subject: [PATCH 15/31] Move lazy var access to the MainActor (#3333) Task/Issue URL: https://app.asana.com/0/1193060753475688/1208204608199357/f Tech Design URL: CC: Description: This PR makes a speculative fix for an lazy var access issue --- DuckDuckGo/AppDelegate.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index a5ea809d6c..9bd1828a58 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -59,6 +59,7 @@ import os.log private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults + @MainActor private lazy var vpnWorkaround: VPNRedditSessionWorkaround = { return VPNRedditSessionWorkaround( accountManager: AppDependencyProvider.shared.accountManager, @@ -576,7 +577,7 @@ import os.log } func applicationWillResignActive(_ application: UIApplication) { - Task { + Task { @MainActor in await refreshShortcuts() await vpnWorkaround.removeRedditSessionWorkaround() } From ddbd2d5ef3eaef6a48a6d03d58ddacb638c814e3 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 11 Sep 2024 10:42:26 +1000 Subject: [PATCH 16/31] Fix Localizable strings (#3341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1206329551987282/1208258905717843/f **Description**: This PR addresses: 1. Issue due to two strings added for the experiment group use the same keys of the control group. The good thing is that from my previous [PR](https://github.com/duckduckgo/iOS/pull/3325/files) I didn’t override any of the old strings so there’s no rush to make a new release build before end of week. 2. Split the title for Address Bar position dialog to two strings to allow specific attributes to the wording. --- DuckDuckGo/UserText.swift | 8 +++++--- DuckDuckGo/en.lproj/Localizable.strings | 14 +++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 2c93e84613..8410fd0612 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1365,10 +1365,10 @@ But if you *do* want a peek under the hood, you can find more information about } enum BrowsersComparison { - public static let title = NSLocalizedString("onboarding.browsers.title", value: "Protections activated!", comment: "The title of the dialog to show the privacy features that DuckDuckGo offers") + 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.browsers.features.trackerBlocker.title", value: "Block 3rd party trackers", comment: "Message to highlight browser capability ofblocking 3rd party trackers") + 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") public static let eraseBrowsingData = NSLocalizedString("onboarding.highlights.browsers.features.eraseBrowsingData.title", value: "Erase browsing data swiftly", comment: "Message to highlight browser capability of swiftly erase browsing data") @@ -1383,7 +1383,8 @@ But if you *do* want a peek under the hood, you can find more information about enum AddressBarPosition { public static let title = NSLocalizedString("onboarding.highlights.addressBarPosition.title", value: "Where should I put your address bar?", comment: "The title of the onboarding dialog popup to select the preferred address bar position.") - public static let topTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.top.title", value: "Top (Default)", comment: "The title of the option to set the address bar to the top.") + public static let topTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.top.title", value: "Top", comment: "The title of the option to set the address bar to the top.") + public static let defaultOption = NSLocalizedString("onboarding.highlights.addressBarPosition.default", value: "(Default)", comment: "Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default)") public static let topMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.top.message", value: "Easy to see", comment: "The message of the option to set the address bar to the top.") public static let bottomTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.title", value: "Bottom", comment: "The title of the option to set the address bar to the bottom.") public static let bottomMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.message", value: "Easy to reach", comment: "The message of the option to set the address bar to the bottom.") @@ -1394,6 +1395,7 @@ But if you *do* want a peek under the hood, you can find more information about 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 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") } enum FireDialog { diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 9ca7809f77..ec91ba69c6 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -779,6 +779,9 @@ /* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ "contextual.onboarding.highlights.try-a-search.message" = "Your DuckDuckGo searches are always private."; +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "baby ducklings"; + /* 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!"; @@ -1847,6 +1850,9 @@ https://duckduckgo.com/mac"; /* The title of the CTA to progress to the next onboarding screen. */ "onboarding.highlights.addressBarPosition.cta" = "Next"; +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(Default)"; + /* The title of the onboarding dialog popup to select the preferred address bar position. */ "onboarding.highlights.addressBarPosition.title" = "Where should I put your address bar?"; @@ -1854,7 +1860,7 @@ https://duckduckgo.com/mac"; "onboarding.highlights.addressBarPosition.top.message" = "Easy to see"; /* The title of the option to set the address bar to the top. */ -"onboarding.highlights.addressBarPosition.top.title" = "Top (Default)"; +"onboarding.highlights.addressBarPosition.top.title" = "Top"; /* The title of the CTA to progress to the next onboarding screen. */ "onboarding.highlights.appIconSelection.cta" = "Next"; @@ -1874,6 +1880,12 @@ https://duckduckgo.com/mac"; /* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Erase browsing data swiftly"; +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Block 3rd party trackers"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Protections activated!"; + /* The title of the fire button CTA to skip erasing the data. */ "onboarding.highlights.fireDialog.cta.skip" = "Skip"; From 4603706f6aca1100b72427ed9416c0d3a264c0b7 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 11 Sep 2024 11:06:38 +1000 Subject: [PATCH 17/31] Alessandro/onboarding choose app icon (#3330) Task/Issue URL:mhttps://app.asana.com/0/1206329551987282/1208084960726996/f **Description**: Add AppIcon screen selection to the onboarding. --- DuckDuckGo.xcodeproj/project.pbxproj | 24 ++++ .../AppIconPicker/AppIconPicker.swift | 73 ++++++++++ .../AppIconPickerViewModel.swift | 64 +++++++++ .../OnboardingView+AppIconPickerContent.swift | 88 ++++++++++++ .../OnboardingIntro/OnboardingView.swift | 21 +-- .../AppIconPickerViewModelTests.swift | 125 ++++++++++++++++++ 6 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift create mode 100644 DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift create mode 100644 DuckDuckGoTests/AppIconPickerViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9fc70bfde3..a2fe6b515a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -696,11 +696,14 @@ 9F69331D2C5A191400CD6A5D /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */; }; 9F69331F2C5B1D0C00CD6A5D /* OnFirstAppearViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331E2C5B1D0C00CD6A5D /* OnFirstAppearViewModifier.swift */; }; 9F6933212C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6933202C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift */; }; + 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */; }; 9F7CFF782C86E3E10012833E /* OnboardingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */; }; + 9F7CFF7D2C89B69A0012833E /* AppIconPickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */; }; 9F8007262C5261AF003EDAF4 /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */; }; 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; 9F9A922E2C86A56B001D036D /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */; }; 9F9A92312C86AAE9001D036D /* OnboardingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */; }; + 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92332C86B42B001D036D /* AppIconPicker.swift */; }; 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */; }; 9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */; }; 9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; }; @@ -713,6 +716,7 @@ 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */; }; 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */; }; + 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.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 */; }; @@ -2476,10 +2480,13 @@ 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTutorialSettings.swift; sourceTree = ""; }; 9F69331E2C5B1D0C00CD6A5D /* OnFirstAppearViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFirstAppearViewModifier.swift; sourceTree = ""; }; 9F6933202C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHostingControllerMock.swift; sourceTree = ""; }; + 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AppIconPickerContent.swift"; sourceTree = ""; }; 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManagerTests.swift; sourceTree = ""; }; + 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModelTests.swift; sourceTree = ""; }; 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyDataReporter.swift; sourceTree = ""; }; 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDebugView.swift; sourceTree = ""; }; + 9F9A92332C86B42B001D036D /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFirePixelMock.swift; sourceTree = ""; }; 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Landing.swift"; sourceTree = ""; }; 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = ""; }; @@ -2491,6 +2498,7 @@ 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LaunchOptionsHandler.swift; path = ../DuckDuckGo/LaunchOptionsHandler.swift; sourceTree = ""; }; 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandlerTests.swift; sourceTree = ""; }; 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; + 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModel.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 = ""; }; @@ -4660,6 +4668,7 @@ 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */, 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */, 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */, + 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */, ); path = OnboardingIntro; sourceTree = ""; @@ -4678,6 +4687,7 @@ 9F4CC51E2C48D758006A96EB /* ContextualDaxDialogsFactoryTests.swift */, 9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */, 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */, + 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */, ); name = Onboarding; sourceTree = ""; @@ -4715,6 +4725,15 @@ name = OnboardingDebugView; sourceTree = ""; }; + 9F9A92322C86B419001D036D /* AppIconPicker */ = { + isa = PBXGroup; + children = ( + 9F9A92332C86B42B001D036D /* AppIconPicker.swift */, + 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */, + ); + path = AppIconPicker; + sourceTree = ""; + }; 9F9EE4CB2C377D2400D4118E /* Mocks */ = { isa = PBXGroup; children = ( @@ -4763,6 +4782,7 @@ 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */ = { isa = PBXGroup; children = ( + 9F9A92322C86B419001D036D /* AppIconPicker */, 9F9A922C2C86A560001D036D /* Manager */, 9FE05CEC2C36423C00D9046B /* Pixels */, 56D060202C356B0B003BAEB5 /* ContextualDaxDialogs */, @@ -7339,6 +7359,7 @@ 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */, 85DFEDED24C7CCA500973FE7 /* AppWidthObserver.swift in Sources */, 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */, + 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */, CB825C962C071C9300BCC586 /* AlertViewPresenter.swift in Sources */, 1EE7C299294227EC0026C8CB /* AutoconsentSettingsViewController.swift in Sources */, 1E8AD1D527C2E22900ABA377 /* DownloadsListSectionViewModel.swift in Sources */, @@ -7449,6 +7470,7 @@ D6E83C602B22B3C9006C8AFB /* SettingsState.swift in Sources */, D6E83C482B20C812006C8AFB /* SettingsHostingController.swift in Sources */, F46FEC5727987A5F0061D9DF /* KeychainItemsDebugViewController.swift in Sources */, + 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */, D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */, BDD3B3552B8EF8DB005857A8 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, BD862E0B2B30F9300073E2EE /* VPNFeedbackFormView.swift in Sources */, @@ -7549,6 +7571,7 @@ 1DEAADFF2BA7832F00E25A97 /* EmailProtectionView.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, + 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultModel.swift in Sources */, @@ -7831,6 +7854,7 @@ 983BD6B52B34760600AAC78E /* MockPrivacyConfiguration.swift in Sources */, 1E8146AD28C8ABF000D1AF63 /* TrackerAnimationLogicTests.swift in Sources */, C1CDA31E2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift in Sources */, + 9F7CFF7D2C89B69A0012833E /* AppIconPickerViewModelTests.swift in Sources */, B6AD9E3A28D456820019CDE9 /* PrivacyConfigurationManagerMock.swift in Sources */, F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */, 6FABAA692C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift in Sources */, diff --git a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift new file mode 100644 index 0000000000..8ff955f2d0 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift @@ -0,0 +1,73 @@ +// +// AppIconPicker.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI + +private enum Metrics { + static let cornerRadius: CGFloat = 13.0 + static let iconSize: CGFloat = 56.0 + static let spacing: CGFloat = 16.0 + static let strokeFrameSize: CGFloat = 60 + static let strokeWidth: CGFloat = 3 + static let strokeInset: CGFloat = 1.5 +} + +struct AppIconPicker: View { + @Environment(\.colorScheme) private var color + + @StateObject private var viewModel = AppIconPickerViewModel() + + let layout = [GridItem(.adaptive(minimum: Metrics.iconSize, maximum: Metrics.iconSize), spacing: Metrics.spacing)] + + var body: some View { + LazyVGrid(columns: layout, spacing: Metrics.spacing) { + ForEach(viewModel.items, id: \.icon) { item in + Image(uiImage: item.icon.mediumImage ?? UIImage()) + .resizable() + .frame(width: Metrics.iconSize, height: Metrics.iconSize) + .cornerRadius(Metrics.cornerRadius) + .overlay { + strokeOverlay(isSelected: item.isSelected) + } + .onTapGesture { + viewModel.changeApp(icon: item.icon) + } + } + } + } + + @ViewBuilder + private func strokeOverlay(isSelected: Bool) -> some View { + if isSelected { + RoundedRectangle(cornerRadius: Metrics.cornerRadius) + .foregroundColor(.clear) + .frame(width: Metrics.strokeFrameSize, height: Metrics.strokeFrameSize) + .overlay( + RoundedRectangle(cornerRadius: Metrics.cornerRadius) + .inset(by: -Metrics.strokeInset) + .stroke(.blue, lineWidth: Metrics.strokeWidth) + ) + } + } +} + +#Preview { + AppIconPicker() +} diff --git a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift new file mode 100644 index 0000000000..ceaebee301 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift @@ -0,0 +1,64 @@ +// +// AppIconPickerViewModel.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 + +@MainActor +final class AppIconPickerViewModel: ObservableObject { + + struct DisplayModel { + let icon: AppIcon + let isSelected: Bool + } + + @Published private(set) var items: [DisplayModel] = [] + + private let appIconManager: AppIconManaging + + init(appIconManager: AppIconManaging = AppIconManager.shared) { + self.appIconManager = appIconManager + items = makeDisplayModels() + } + + func changeApp(icon: AppIcon) { + appIconManager.changeAppIcon(icon) { [weak self] error in + guard let self, error == nil else { return } + items = makeDisplayModels() + } + } + + private func makeDisplayModels() -> [DisplayModel] { + AppIcon.allCases.map { appIcon in + DisplayModel(icon: appIcon, isSelected: appIconManager.appIcon == appIcon) + } + } +} + +protocol AppIconManaging { + var appIcon: AppIcon { get } + func changeAppIcon(_ appIcon: AppIcon, completionHandler: ((Error?) -> Void)?) +} + +extension AppIconManaging { + func changeAppIcon(_ appIcon: AppIcon) { + changeAppIcon(appIcon, completionHandler: nil) + } +} + +extension AppIconManager: AppIconManaging {} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift new file mode 100644 index 0000000000..3d2f8e19bb --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift @@ -0,0 +1,88 @@ +// +// OnboardingView+AppIconPickerContent.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI +import Onboarding + +extension OnboardingView { + + struct AppIconPickerContentState { + var animateTitle = true + var animateMessage = false + var showContent = false + } + + struct AppIconPickerContent: View { + + private var animateTitle: Binding + private var animateMessage: Binding + private var showContent: Binding + private let action: () -> Void + + init( + animateTitle: Binding = .constant(true), + animateMessage: Binding = .constant(true), + showContent: Binding = .constant(false), + action: @escaping () -> Void + ) { + self.animateTitle = animateTitle + self.animateMessage = animateMessage + self.showContent = showContent + self.action = action + } + + var body: some View { + VStack(spacing: 16.0) { + AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AppIconSelection.title, startAnimating: animateTitle) { + animateMessage.wrappedValue = true + } + .foregroundColor(.primary) + .font(Metrics.titleFont) + + AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AppIconSelection.message, startAnimating: animateMessage) { + withAnimation { + showContent.wrappedValue = true + } + } + .foregroundColor(.primary) + .font(Metrics.messageFont) + + VStack(spacing: 24) { + AppIconPicker() + .offset(x: Metrics.pickerLeadingOffset) // Remove left padding for the first item + + Button(action: action) { + Text(UserText.HighlightsOnboardingExperiment.AppIconSelection.cta) + } + .buttonStyle(PrimaryButtonStyle()) + } + .visibility(showContent.wrappedValue ? .visible : .invisible) + } + } + + } + +} + +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.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 02f280319f..2f8f04508c 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -39,6 +39,8 @@ struct OnboardingView: View { @State private var showComparisonButton = false @State private var animateComparisonText = false + @State private var appIconPickerContentState = AppIconPickerContentState() + init(model: OnboardingIntroViewModel) { self.model = model } @@ -72,6 +74,10 @@ struct OnboardingView: View { case .browsersComparisonDialog: showComparisonButton = true animateComparisonText = false + case .chooseAppIconDialog: + appIconPickerContentState.animateTitle = false + appIconPickerContentState.animateMessage = false + appIconPickerContentState.showContent = true default: break } } @@ -140,15 +146,12 @@ struct OnboardingView: View { } private var appIconPickerView: some View { - // TODO: Implement View - VStack(spacing: 30) { - Text(verbatim: "Choose App Icon") - - Button(action: model.appIconPickerContinueAction) { - Text(verbatim: "Next") - } - .buttonStyle(PrimaryButtonStyle()) - } + AppIconPickerContent( + animateTitle: $appIconPickerContentState.animateTitle, + animateMessage: $appIconPickerContentState.animateMessage, + showContent: $appIconPickerContentState.showContent, + action: model.appIconPickerContinueAction + ) .onboardingDaxDialogStyle() } diff --git a/DuckDuckGoTests/AppIconPickerViewModelTests.swift b/DuckDuckGoTests/AppIconPickerViewModelTests.swift new file mode 100644 index 0000000000..76088f3ded --- /dev/null +++ b/DuckDuckGoTests/AppIconPickerViewModelTests.swift @@ -0,0 +1,125 @@ +// +// AppIconPickerViewModelTests.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 AppIconPickerViewModelTests: XCTestCase { + private var sut: AppIconPickerViewModel! + private var appIconManagerMock: AppIconManagerMock! + + @MainActor + override func setUpWithError() throws { + try super.setUpWithError() + + appIconManagerMock = AppIconManagerMock() + sut = AppIconPickerViewModel(appIconManager: appIconManagerMock) + } + + override func tearDownWithError() throws { + appIconManagerMock = nil + sut = nil + try super.tearDownWithError() + } + + @MainActor + func testWhenItemsIsCalledThenIconsAreReturned() { + // GIVEN + let expectedIcons: [AppIcon] = [.red, .yellow, .green, .blue, .purple, .black] + + // WHEN + let result = sut.items + + // THEN + XCTAssertEqual(result.map(\.icon), expectedIcons) + } + + @MainActor + func testWhenInitThenSelectedAppIconIsReturned() { + // GIVEN + appIconManagerMock.appIcon = .purple + sut = AppIconPickerViewModel(appIconManager: appIconManagerMock) + + // WHEN + let result = sut.items + + // THEN + XCTAssertEqual(result.count, AppIcon.allCases.count) + assertSelected(.purple, items: result) + } + + @MainActor + func testWhenChangeAppIconIsCalledAndManagerFailsThenSelectedAppIconIsNotUpdated() { + // GIVEN + appIconManagerMock.appIcon = .red + appIconManagerMock.changeAppIconError = NSError(domain: #function, code: 0) + assertSelected(.red, items: sut.items) + + // WHEN + sut.changeApp(icon: .purple) + + // THEN + assertSelected(.red, items: sut.items) + } + + @MainActor + func testWhenChangeAppIconIsCalledThenShouldAskAppIconManagerToChangeAppIcon() { + // GIVEN + XCTAssertFalse(appIconManagerMock.didCallChangeAppIcon) + XCTAssertNil(appIconManagerMock.capturedAppIcon) + + // WHEN + sut.changeApp(icon: .purple) + + // THEN + XCTAssertTrue(appIconManagerMock.didCallChangeAppIcon) + XCTAssertEqual(appIconManagerMock.capturedAppIcon, .purple) + } + + private func assertSelected(_ appIcon: AppIcon, items: [AppIconPickerViewModel.DisplayModel]) { + items.forEach { model in + if model.icon == appIcon { + XCTAssertTrue(model.isSelected) + } else { + XCTAssertFalse(model.isSelected) + } + } + } +} + +final class AppIconManagerMock: AppIconManaging { + private(set) var didCallChangeAppIcon = false + private(set) var capturedAppIcon: AppIcon? + + var appIcon: DuckDuckGo.AppIcon = .red + + var changeAppIconError: Error? + + func changeAppIcon(_ appIcon: AppIcon, completionHandler: (((any Error)?) -> Void)?) { + didCallChangeAppIcon = true + capturedAppIcon = appIcon + + if let changeAppIconError { + completionHandler?(changeAppIconError) + } else { + completionHandler?(nil) + } + } + +} From 4289d775e9734ade2305c97a63913f4823ff7d38 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 11 Sep 2024 12:42:28 +0200 Subject: [PATCH 18/31] Improve Data Store ID managing (#3335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/856498667320406/1207976962313181/f Tech Design URL: CC: **Description**: Improve the way we manage Website Data Store IDs to reduce a chance of race conditions. **Steps to test this PR**: Validate if data clearing works iOS 16/17): - Cookie clearing. - Fireproofing. - AutoClear. Migration from previous version should reuse container ID. Fresh install should allocate container ID. **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **OS Testing**: * [x] iOS 15 * [x] iOS 16 * [x] iOS 17 --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/UserDefaultsPropertyWrapper.swift | 2 - Core/WKWebViewConfigurationExtension.swift | 36 +++++----- Core/WebCacheManager.swift | 27 ++++--- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++ .../xcschemes/DuckDuckGo.xcscheme | 1 + DuckDuckGoTests/DataStoreIdManagerTests.swift | 70 +++++++++++++++++++ .../FireButtonReferenceTests.swift | 11 +-- DuckDuckGoTests/WebCacheManagerTests.swift | 40 +++-------- 8 files changed, 124 insertions(+), 67 deletions(-) create mode 100644 DuckDuckGoTests/DataStoreIdManagerTests.swift diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 4f221722cc..11fcd46d61 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -134,8 +134,6 @@ public struct UserDefaultsWrapper { case addressBarPosition = "com.duckduckgo.ios.addressbarposition" case showFullURLAddress = "com.duckduckgo.ios.showfullurladdress" - case webContainerId = "com.duckduckgo.ios.webcontainer.id" - case bookmarksLastGoodVersion = "com.duckduckgo.ios.bookmarksLastGoodVersion" case bookmarksMigrationVersion = "com.duckduckgo.ios.bookmarksMigrationVersion" diff --git a/Core/WKWebViewConfigurationExtension.swift b/Core/WKWebViewConfigurationExtension.swift index 32a34285b2..18de2c2dad 100644 --- a/Core/WKWebViewConfigurationExtension.swift +++ b/Core/WKWebViewConfigurationExtension.swift @@ -18,15 +18,16 @@ // import WebKit +import Persistence extension WKWebViewConfiguration { @MainActor - public static func persistent(idManager: DataStoreIdManager = .shared) -> WKWebViewConfiguration { + public static func persistent(idManager: DataStoreIdManaging = DataStoreIdManager.shared) -> WKWebViewConfiguration { let config = configuration(persistsData: true) // Only use a container if there's an id which will be allocated next time the fire button is used. - if #available(iOS 17, *), let containerId = idManager.id { + if #available(iOS 17, *), let containerId = idManager.currentId { config.websiteDataStore = WKWebsiteDataStore(forIdentifier: containerId) } return config @@ -58,32 +59,33 @@ extension WKWebViewConfiguration { public protocol DataStoreIdManaging { - var id: UUID? { get } - var hasId: Bool { get } - func allocateNewContainerId() + var currentId: UUID? { get } + func invalidateCurrentIdAndAllocateNew() } public class DataStoreIdManager: DataStoreIdManaging { - public static let shared = DataStoreIdManager() + enum Constants: String { + case currentWebContainerId = "com.duckduckgo.ios.webcontainer.id" + } - @UserDefaultsWrapper(key: .webContainerId, defaultValue: nil) - private var containerId: String? + public static let shared = DataStoreIdManager() - public var id: UUID? { - if let containerId { - return UUID(uuidString: containerId) - } - return nil + private let store: KeyValueStoring + init(store: KeyValueStoring = UserDefaults.app) { + self.store = store } - public var hasId: Bool { - return containerId != nil + public var currentId: UUID? { + guard let uuidString = store.object(forKey: Constants.currentWebContainerId.rawValue) as? String else { + return nil + } + return UUID(uuidString: uuidString) } - public func allocateNewContainerId() { - self.containerId = UUID().uuidString + public func invalidateCurrentIdAndAllocateNew() { + store.set(UUID().uuidString, forKey: Constants.currentWebContainerId.rawValue) } } diff --git a/Core/WebCacheManager.swift b/Core/WebCacheManager.swift index 356eac3c0d..c43462a36c 100644 --- a/Core/WebCacheManager.swift +++ b/Core/WebCacheManager.swift @@ -24,8 +24,8 @@ import os.log extension WKWebsiteDataStore { - public static func current(dataStoreIdManager: DataStoreIdManager = .shared) -> WKWebsiteDataStore { - if #available(iOS 17, *), let id = dataStoreIdManager.id { + public static func current(dataStoreIdManager: DataStoreIdManaging = DataStoreIdManager.shared) -> WKWebsiteDataStore { + if #available(iOS 17, *), let id = dataStoreIdManager.currentId { return WKWebsiteDataStore(forIdentifier: id) } else { return WKWebsiteDataStore.default() @@ -82,17 +82,13 @@ public class WebCacheManager { dataStoreIdManager: DataStoreIdManaging = DataStoreIdManager.shared) async { var cookiesToUpdate = [HTTPCookie]() - if #available(iOS 17, *), dataStoreIdManager.hasId { + if #available(iOS 17, *) { cookiesToUpdate += await containerBasedClearing(storeIdManager: dataStoreIdManager) ?? [] } // Perform legacy clearing to migrate to new container cookiesToUpdate += await legacyDataClearing() ?? [] - if #available(iOS 17, *) { - dataStoreIdManager.allocateNewContainerId() - } - cookieStorage.updateCookies(cookiesToUpdate, keepingPreservedLogins: logins) } @@ -118,13 +114,24 @@ extension WebCacheManager { @available(iOS 17, *) private func containerBasedClearing(storeIdManager: DataStoreIdManaging) async -> [HTTPCookie]? { - guard let containerId = storeIdManager.id else { return [] } + guard let containerId = storeIdManager.currentId else { + storeIdManager.invalidateCurrentIdAndAllocateNew() + return [] + } + storeIdManager.invalidateCurrentIdAndAllocateNew() + var dataStore: WKWebsiteDataStore? = WKWebsiteDataStore(forIdentifier: containerId) let cookies = await dataStore?.httpCookieStore.allCookies() dataStore = nil - let uuids = await WKWebsiteDataStore.allDataStoreIdentifiers - let previousLeftOversCount = max(0, uuids.count - 1) // -1 because there should be a current store + var uuids = await WKWebsiteDataStore.allDataStoreIdentifiers + if let newContainerID = storeIdManager.currentId, + let newIdIndex = uuids.firstIndex(of: newContainerID) { + assertionFailure("Attempted to cleanup current Data Store") + uuids.remove(at: newIdIndex) + } + + let previousLeftOversCount = max(0, uuids.count - 1) // -1 because one store is expected to be cleared for uuid in uuids { try? await WKWebsiteDataStore.remove(forIdentifier: uuid) } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a2fe6b515a..6e7bb29455 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -569,6 +569,7 @@ 980891A92238504B00313A70 /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980891A82238504B00313A70 /* UILabelExtension.swift */; }; 9813F79822BA71AA00A80EDB /* StorageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9813F79722BA71AA00A80EDB /* StorageCache.swift */; }; 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9817C9C221EF594700884F65 /* AutoClear.swift */; }; + 981C49B02C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */; }; 981CA7EA2617797500E119D5 /* MainViewController+AddFavoriteFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981CA7E92617797500E119D5 /* MainViewController+AddFavoriteFlow.swift */; }; 981FED692201FE69008488D7 /* AutoClearSettingsScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981FED682201FE69008488D7 /* AutoClearSettingsScreenTests.swift */; }; 981FED6E22025151008488D7 /* BlankSnapshotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981FED6C22025151008488D7 /* BlankSnapshotViewController.swift */; }; @@ -1861,6 +1862,7 @@ 981685572521EEF600FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/MainInterface.strings; sourceTree = ""; }; 981685A825221ACF00FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = ""; }; 9817C9C221EF594700884F65 /* AutoClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoClear.swift; sourceTree = ""; }; + 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreIdManagerTests.swift; sourceTree = ""; }; 981CA7E92617797500E119D5 /* MainViewController+AddFavoriteFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+AddFavoriteFlow.swift"; sourceTree = ""; }; 981DCA922521EFAB00CD4C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; 981DCA932521EFAB00CD4C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -5948,6 +5950,7 @@ 834DF990248FDDF60075EA48 /* UserAgentTests.swift */, 8540BD5123D8C2220057FDD2 /* PreserveLoginsTests.swift */, 850559D123CF710C0055C0D5 /* WebCacheManagerTests.swift */, + 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */, F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */, 85AD49ED2B6149110085D2D1 /* CookieStorageTests.swift */, ); @@ -7794,6 +7797,7 @@ CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */, D6F557BA2C8859040034444B /* DuckPlayerExperimentTests.swift in Sources */, 986B45D0299E30A50089D2D7 /* BookmarkEntityTests.swift in Sources */, + 981C49B02C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift in Sources */, B6AD9E3828D4512E0019CDE9 /* EmbeddedTrackerDataTests.swift in Sources */, 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */, 9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index aa78a92807..b9ef06c977 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -27,6 +27,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO" + language = "en" codeCoverageEnabled = "YES"> .Key.webContainerId.rawValue) if #available(iOS 17, *) { WKWebsiteDataStore.fetchAllDataStoreIdentifiers { uuids in uuids.forEach { @@ -47,21 +44,19 @@ class WebCacheManagerTests: XCTestCase { let logins = MockPreservedLogins(domains: []) let storage = CookieStorage() - let inMemoryDataStoreIdManager = DataStoreIdManager() - XCTAssertFalse(inMemoryDataStoreIdManager.hasId) - XCTAssertNil(inMemoryDataStoreIdManager.id) + let inMemoryDataStoreIdManager = DataStoreIdManager(store: MockKeyValueStore()) + XCTAssertNil(inMemoryDataStoreIdManager.currentId) await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: inMemoryDataStoreIdManager) - XCTAssertTrue(inMemoryDataStoreIdManager.hasId) - let oldId = inMemoryDataStoreIdManager.id?.uuidString + XCTAssertNotNil(inMemoryDataStoreIdManager.currentId) + let oldId = inMemoryDataStoreIdManager.currentId?.uuidString XCTAssertNotNil(oldId) await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: inMemoryDataStoreIdManager) - XCTAssertTrue(inMemoryDataStoreIdManager.hasId) - XCTAssertNotNil(inMemoryDataStoreIdManager.id?.uuidString) - XCTAssertNotEqual(inMemoryDataStoreIdManager.id?.uuidString, oldId) + XCTAssertNotNil(inMemoryDataStoreIdManager.currentId) + XCTAssertNotEqual(inMemoryDataStoreIdManager.currentId?.uuidString, oldId) } @available(iOS 17, *) @@ -87,7 +82,7 @@ class WebCacheManagerTests: XCTestCase { XCTAssertEqual(5, loadedCount) let cookieStore = CookieStorage() - await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager.shared) + await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let cookies = await defaultStore.httpCookieStore.allCookies() XCTAssertEqual(cookies.count, 0) @@ -136,7 +131,7 @@ class WebCacheManagerTests: XCTestCase { await WebCacheManager.shared.clear(cookieStorage: cookieStorage, logins: logins, - dataStoreIdManager: DataStoreIdManager.shared) + dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let cookies = await defaultStore.httpCookieStore.allCookies() XCTAssertEqual(cookies.count, 0) @@ -166,7 +161,7 @@ class WebCacheManagerTests: XCTestCase { let storage = CookieStorage() storage.isConsumed = true - await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: DataStoreIdManager.shared) + await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) XCTAssertEqual(storage.cookies.count, 2) XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "duckduckgo.com" })) @@ -193,7 +188,7 @@ class WebCacheManagerTests: XCTestCase { let cookieStore = CookieStorage() - await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager.shared) + await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let cookies = await defaultStore.httpCookieStore.allCookies() XCTAssertEqual(cookies.count, 0) @@ -225,16 +220,3 @@ class WebCacheManagerTests: XCTestCase { } } - -class InMemoryDataStoreIdManager: DataStoreIdManaging { - - var id: UUID? - - var hasId: Bool { - id != nil - } - - func allocateNewContainerId() { - id = UUID() - } -} From 568e0529039a54b277dee3a58ef0c8f9cb982000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Wed, 11 Sep 2024 15:54:30 +0200 Subject: [PATCH 19/31] Fix PrivacyDashboard appearance on entering foreground (#3345) Task/Issue URL: https://app.asana.com/0/414709148257752/1208197595138828/f Tech Design URL: CC: **Description**: `PrivacyDashboardViewController` used a shared `interfaceStyle` accessor when window was not yet visible on entering foreground. This caused a fallback to `.light` color appearance. **Steps to test this PR**: 1. Enable dark mode 2. Open Privacy Dashboard 3. Move app to background, 4. Go back to the app - Privacy Dashboard should keep the dark appearance. **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Theme Testing**: * [x] Light theme * [x] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- .../PrivacyDashboardViewController.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift index 0bb500669b..f16e209939 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift @@ -171,14 +171,14 @@ extension PrivacyDashboardViewController { private func decorate() { let theme = ThemeManager.shared.currentTheme view.backgroundColor = theme.privacyDashboardWebviewBackgroundColor - privacyDashboardController.theme = .init(theme) + privacyDashboardController.theme = .init(traitCollection) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - privacyDashboardController.theme = .init() + privacyDashboardController.theme = .init(traitCollection) } } } @@ -378,20 +378,12 @@ extension PrivacyDashboardViewController { } private extension PrivacyDashboardTheme { - init(_ userInterfaceStyle: UIUserInterfaceStyle = ThemeManager.shared.currentInterfaceStyle) { - switch userInterfaceStyle { + init(_ traitCollection: UITraitCollection) { + switch traitCollection.userInterfaceStyle { case .light: self = .light case .dark: self = .dark case .unspecified: self = .light @unknown default: self = .light } } - - init(_ theme: Theme) { - switch theme.name { - case .light: self = .light - case .dark: self = .dark - case .systemDefault: self.init(ThemeManager.shared.currentInterfaceStyle) - } - } } From 29e2b7934465ea16c2d3f9894ef272bea8df9a13 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 12 Sep 2024 09:50:26 +1000 Subject: [PATCH 20/31] Onboarding Intro - Add choose address bar position (#3340) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208084960726997/f **Description**: Add Address Bar position selection screen to the onboarding. --- Core/NSAttributedStringExtension.swift | 82 ++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 24 +++ .../address-bar-bottom.imageset/Contents.json | 12 ++ .../address-bar-bottom.pdf | Bin 0 -> 3380 bytes .../address-bar-top.imageset/Contents.json | 12 ++ .../address-bar-top.pdf | Bin 0 -> 3487 bytes .../checkShape.imageset/Contents.json | 15 ++ .../checkShape.imageset/Shape.pdf | Bin 0 -> 1211 bytes .../OnboardingAddressBarPositionPicker.swift | 186 ++++++++++++++++++ ...ingAddressBarPositionPickerViewModel.swift | 104 ++++++++++ .../OnboardingIntroViewModel.swift | 18 +- ...ardingView+AddressBarPositionContent.swift | 79 ++++++++ .../OnboardingIntro/OnboardingView.swift | 15 +- ...dressBarPositionPickerViewModelTests.swift | 93 +++++++++ .../OnboardingIntroViewModelTests.swift | 117 +++++++++-- 15 files changed, 729 insertions(+), 28 deletions(-) create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/address-bar-bottom.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/address-bar-top.pdf create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json create mode 100644 DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Shape.pdf create mode 100644 DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift create mode 100644 DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift create mode 100644 DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift diff --git a/Core/NSAttributedStringExtension.swift b/Core/NSAttributedStringExtension.swift index f0a3a61be7..c4beb2b327 100644 --- a/Core/NSAttributedStringExtension.swift +++ b/Core/NSAttributedStringExtension.swift @@ -40,4 +40,86 @@ extension NSAttributedString { newString.setAttributes([.font: newFont], range: string.fullRange) return newString } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified font. + /// + /// - Parameter font: The `UIFont` to apply to the text in the `NSAttributedString`. + /// - Returns: A new `NSAttributedString`initialized with characters and attributes of the current attributed string plus the specified font. + public func withFont(_ font: UIFont) -> NSAttributedString { + with(attribute: .font, value: font) + } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified text color + /// + /// - Parameter color: The color to apply to the text + /// - Returns: A new `NSAttributedString` initialized with characters and attributes of the current attributed string plus the text color + public func withTextColor(_ color: UIColor) -> NSAttributedString { + with(attribute: .foregroundColor, value: color) + } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified attribute + /// + /// - Parameters: + /// - key: The attribute key to apply. This should be one of the keys defined in `NSAttributedString.Key`. + /// - value: The value associated with the attribute key. This can be any object compatible with the attribute. + /// - range: An optional `NSRange` specifying the range within the `NSAttributedString` to apply the attribute. + /// If `nil`, the attribute is applied to the entire `NSAttributedString`. + /// - Returns: A new `NSAttributedString` with the specified attribute applied. + public func with(attribute key: NSAttributedString.Key, value: Any, in range: NSRange? = nil) -> NSAttributedString { + with(attributes: [key: value], in: range) + } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified attributes + /// + /// - Parameters: + /// - attributes: A dictionary of attributes to apply, where the keys are of type `NSAttributedString.Key` and the values + /// are objects compatible with the attributes (e.g., `UIFont`, `UIColor`). + /// - range: An optional `NSRange` specifying the range within the `NSAttributedString` to apply the attributes. + /// If `nil`, the attributes are applied to the entire `NSAttributedString`. + /// - Returns: A new `NSAttributedString` with the specified attributes applied. + public func with(attributes: [NSAttributedString.Key: Any], in range: NSRange? = nil) -> NSAttributedString { + let mutableString = NSMutableAttributedString(attributedString: self) + mutableString.addAttributes(attributes, range: range ?? string.nsRange) + return mutableString + } +} + +/// Concatenates two `NSAttributedString` instances, returning a new `NSAttributedString`. +/// +/// - Parameters: +/// - lhs: The left-hand side `NSAttributedString` to which the `rhs` `NSAttributedString` will be appended. +/// - rhs: The `NSAttributedString` to append to the `lhs` `NSAttributedString`. +/// - Returns: A new `NSAttributedString` that is the result of concatenating `lhs` and `rhs`. +public func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { + let mutable = NSMutableAttributedString(attributedString: lhs) + mutable.append(rhs) + return mutable +} + +/// Concatenates an `NSAttributedString` with a `String`, returning a new `NSAttributedString`. +/// +/// - Parameters: +/// - lhs: The left-hand side `NSAttributedString` to which the `String` will be appended. +/// - rhs: The `String` to append to the `lhs` `NSAttributedString`. +/// - Returns: A new `NSAttributedString` which is the result of concatenating `lhs` with `rhs`. +public func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString { + lhs + NSAttributedString(string: rhs) +} + +/// Concatenates a `String` with an `NSAttributedString`, returning a new `NSAttributedString`. +/// +/// - Parameters: +/// - lhs: The `String` to prepend to the `rhs` `NSAttributedString`. +/// - rhs: The right-hand side `NSAttributedString` that will be appended to the `lhs` `String`. +/// - Returns: A new `NSAttributedString` which is the result of concatenating `lhs` with `rhs`. +public func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString { + NSAttributedString(string: lhs) + rhs +} + +private extension String { + + var nsRange: NSRange { + NSRange(startIndex..., in: self) + } + } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6e7bb29455..ed5f16c3c8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -700,6 +700,7 @@ 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */; }; 9F7CFF782C86E3E10012833E /* OnboardingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */; }; 9F7CFF7D2C89B69A0012833E /* AppIconPickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */; }; + 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */; }; 9F8007262C5261AF003EDAF4 /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */; }; 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; 9F9A922E2C86A56B001D036D /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */; }; @@ -717,7 +718,10 @@ 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */; }; 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */; }; + 9FDEC7B42C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */; }; 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 */; }; 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 */; }; @@ -2485,6 +2489,7 @@ 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AppIconPickerContent.swift"; sourceTree = ""; }; 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManagerTests.swift; sourceTree = ""; }; 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModelTests.swift; sourceTree = ""; }; + 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddressBarPositionContent.swift"; sourceTree = ""; }; 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyDataReporter.swift; sourceTree = ""; }; 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDebugView.swift; sourceTree = ""; }; @@ -2500,7 +2505,10 @@ 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LaunchOptionsHandler.swift; path = ../DuckDuckGo/LaunchOptionsHandler.swift; sourceTree = ""; }; 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandlerTests.swift; sourceTree = ""; }; 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; + 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPickerViewModelTests.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -4671,6 +4679,7 @@ 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */, 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */, 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */, + 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */, ); path = OnboardingIntro; sourceTree = ""; @@ -4690,6 +4699,7 @@ 9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */, 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */, 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */, + 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */, ); name = Onboarding; sourceTree = ""; @@ -4756,6 +4766,15 @@ path = BrowsersComparison; sourceTree = ""; }; + 9FDEC7BD2C9125EC00C7A692 /* AddressBarPositionPicker */ = { + isa = PBXGroup; + children = ( + 9FDEC7BE2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift */, + 9FDEC7C02C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift */, + ); + path = AddressBarPositionPicker; + sourceTree = ""; + }; 9FE05CEC2C36423C00D9046B /* Pixels */ = { isa = PBXGroup; children = ( @@ -4784,6 +4803,7 @@ 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */ = { isa = PBXGroup; children = ( + 9FDEC7BD2C9125EC00C7A692 /* AddressBarPositionPicker */, 9F9A92322C86B419001D036D /* AppIconPicker */, 9F9A922C2C86A560001D036D /* Manager */, 9FE05CEC2C36423C00D9046B /* Pixels */, @@ -7350,6 +7370,7 @@ 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, C1641EB32BC2F53C0012607A /* ImportPasswordsViewModel.swift in Sources */, + 9FDEC7BF2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */, @@ -7466,6 +7487,7 @@ 9F9A92312C86AAE9001D036D /* OnboardingDebugView.swift in Sources */, 1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */, C1836CE12C359EC90016D057 /* AutofillBreakageReportCellContentView.swift in Sources */, + 9FDEC7C12C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift in Sources */, 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */, 85374D3C21AC41E700FF5A1E /* FavoritesHomeViewSectionRenderer.swift in Sources */, 85DFEDF124C7EEA400973FE7 /* LargeOmniBarState.swift in Sources */, @@ -7709,6 +7731,7 @@ 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, + 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, F16390821E648B7A005B4550 /* HomeViewController.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, @@ -7782,6 +7805,7 @@ C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */, 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */, 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */, + 9FDEC7B42C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift in Sources */, 85BA58581F34F72F00C6E8CA /* AppUserDefaultsTests.swift in Sources */, F1134EBC1F40D45700B73467 /* MockStatisticsStore.swift in Sources */, 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */, diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json new file mode 100644 index 0000000000..c753560d35 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "address-bar-bottom.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/address-bar-bottom.pdf b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/address-bar-bottom.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bebdafcbae34d24dab5c9c0706ed0e426a9de4cd GIT binary patch literal 3380 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json new file mode 100644 index 0000000000..d99cb4b5de --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "address-bar-top.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/address-bar-top.pdf b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/address-bar-top.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cfcc9f793aa0cd8b60f46c9987063e0790d239c4 GIT binary patch literal 3487 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json new file mode 100644 index 0000000000..bd1c3bd86b --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Shape.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Shape.pdf b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Shape.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de47036a6ae19726c736a989d596fbc42f7c2321 GIT binary patch literal 1211 zcmZvcO>fjN5Qgvm6?3W79_o1fZKsQdmy5S^XT`MJpLyex&@=hesD-VOI;0B7Y?Kdf^7Agas! zd(q_Cy9=6J@2{F({1PnHdXU;pPRH4+du$b(K+VIm>3D9O^i3<6rH|ILr67Y&#xTJ~ zOE%hCM^S1HF)9L5DOsz5Qt&BgDyFPrN|dD4HmXET@yA*l=Ky2S9zIw}h~S(8N-SOo=39U?=8l^i%34NJ3O#jOh*qlF41 zZAww&ZlzH!CJot9JLk(Ow$UrUaRJ?HqAZk_EFB8!N+6V2`w|*U?}{fKOhP%GRGTY7 zJe{=huzTg6mf~LAidK9PrPa25v}?N3Uz=uh!Hlg4anCJUy=mKS=;`JgS{9@F`sZh+ zYFgjrE%=n@i~6E_q8o^N*@2}aHcgqN$0SACIhy6Zdt5cSr}8FdTW^!D9de6+NOO)^;cc+F`y;$az!2hLs xe~EG&_uWH2q2tB;WK)!C)^!7wgx3YGt`~nZ)UW?D^=eqBladlgM{hr0{R0cb2($nI literal 0 HcmV?d00001 diff --git a/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift new file mode 100644 index 0000000000..51e784fde0 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift @@ -0,0 +1,186 @@ +// +// OnboardingAddressBarPositionPicker.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OnboardingAddressBarPositionPicker: View { + @StateObject private var viewModel = OnboardingAddressBarPositionPickerViewModel() + + var body: some View { + VStack(spacing: 16.0) { + ForEach(viewModel.items, id: \.type) { item in + AddressBarPositionButton( + icon: item.icon, + title: AttributedString(item.title), + message: item.message, + isSelected: item.isSelected, + action: { + viewModel.setAddressBar(position: item.type) + } + ) + } + } + } +} + +// MARK: - Views + +private enum Metrics { + enum Button { + static let messageFont = Font.system(size: 15) + static let overlayRadius: CGFloat = 13.0 + static let overlayStroke: CGFloat = 1 + static let itemSpacing: CGFloat = 16.0 + } + enum Checkbox { + static let size: CGFloat = 24.0 + static let checkSize: CGSize = CGSize(width: 12, height: 10) + static let strokeInset = 0.75 + static let strokeWidth = 1.5 + } +} + +extension OnboardingAddressBarPositionPicker { + + struct AddressBarPositionButton: View { + @Environment(\.colorScheme) private var colorScheme + + let icon: ImageResource + let title: AttributedString + let message: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: Metrics.Button.itemSpacing) { + Image(icon) + + VStack(alignment: .leading) { + Text(title) + Text(message) + .font(Metrics.Button.messageFont) + .foregroundStyle(Color.secondary) + } + + Spacer() + + Checkbox(isSelected: isSelected) + } + } + .overlay { + RoundedRectangle(cornerRadius: Metrics.Button.overlayRadius) + .stroke(.blue, lineWidth: Metrics.Button.overlayStroke) + } + .buttonStyle(AddressBarPostionButtonStyle()) + } + + } + +} + +extension OnboardingAddressBarPositionPicker.AddressBarPositionButton { + + struct Checkbox: View { + @Environment(\.colorScheme) private var colorScheme + + let isSelected: Bool + + var body: some View { + Circle() + .frame(width: Metrics.Checkbox.size, height: Metrics.Checkbox.size) + .foregroundColor(foregroundColor) + .overlay { + selectionOverlay + } + } + + @ViewBuilder + private var selectionOverlay: some View { + if isSelected { + Image(.checkShape) + .resizable() + .frame(width: Metrics.Checkbox.checkSize.width, height: Metrics.Checkbox.checkSize.height) + .foregroundColor(.white) + } else { + Circle() + .inset(by: Metrics.Checkbox.strokeInset) + .stroke(.secondary, lineWidth: Metrics.Checkbox.strokeWidth) + } + } + + private var foregroundColor: Color { + switch (colorScheme, isSelected) { + case (.light, true), (.dark, true): + Color.init(designSystemColor: .accent) + case (.light, false): + .black.opacity(0.03) + case (.dark, false): + .white.opacity(0.06) + default: + .clear + } + } + } + +} + +// MARK: - Style + +private struct AddressBarPostionButtonStyle: ButtonStyle { + @Environment(\.colorScheme) private var colorScheme + + private let minHeight = 63.0 + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) + .lineLimit(nil) + .padding() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: minHeight) + .background(backgroundColor(configuration.isPressed)) + .cornerRadius(8) + .contentShape(Rectangle()) // Makes whole button area tappable, when there's no background + } + + private func foregroundColor(_ isPressed: Bool) -> Color { + switch (colorScheme, isPressed) { + case (.dark, false): + return .blue30 + case (.dark, true): + return .blue20 + case (_, false): + return .blueBase + case (_, true): + return .blue70 + } + } + + private func backgroundColor(_ isPressed: Bool) -> Color { + switch (colorScheme, isPressed) { + case (.light, true): + return .blueBase.opacity(0.2) + case (.dark, true): + return .blue30.opacity(0.2) + default: + return .clear + } + } +} diff --git a/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift new file mode 100644 index 0000000000..595fb5c061 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift @@ -0,0 +1,104 @@ +// +// OnboardingAddressBarPositionPickerViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core + +final class OnboardingAddressBarPositionPickerViewModel: ObservableObject { + + struct DisplayModel { + let type: AddressBarPosition + let icon: ImageResource + let title: NSAttributedString + let message: String + let isSelected: Bool + } + + @Published private(set) var items: [DisplayModel] = [] + + private let addressBarPositionManager: AddressBarPositionManaging + + init(addressBarPositionManager: AddressBarPositionManaging = AppUserDefaults()) { + self.addressBarPositionManager = addressBarPositionManager + makeDisplayModels() + } + + func setAddressBar(position: AddressBarPosition) { + addressBarPositionManager.currentAddressBarPosition = position + makeDisplayModels() + } + + private func makeDisplayModels() { + items = AddressBarPosition.allCases.map { addressBarPosition in + let info = addressBarPosition.titleAndMessage + + return DisplayModel( + type: addressBarPosition, + icon: addressBarPosition.image, + title: info.title, + message: info.message, + isSelected: addressBarPositionManager.currentAddressBarPosition == addressBarPosition + ) + } + } +} + +// MARK: - AddressBarPositionManaging + +protocol AddressBarPositionManaging: AnyObject { + var currentAddressBarPosition: AddressBarPosition { get set } +} + +extension AppUserDefaults: AddressBarPositionManaging {} + +// MARK: - AddressBarPosition Helpers + +private extension AddressBarPosition { + + var titleAndMessage: (title: NSAttributedString, message: String) { + switch self { + case .top: + let firstPart = NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle) + .withFont(UIFont.daxBodyBold()) + .withTextColor(UIColor.label) + let secondPart = NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) + .withFont(UIFont.daxBodyRegular()) + .withTextColor(UIColor.secondaryLabel) + + return ( + firstPart + " " + secondPart, + UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage + ) + case .bottom: + return ( + NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) + .withFont(UIFont.daxBodyBold()), + UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage + ) + } + } + + var image: ImageResource { + switch self { + case .top: .addressBarTop + case .bottom: .addressBarBottom + } + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index 15839ee5ab..123fb7eda4 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -29,17 +29,24 @@ final class OnboardingIntroViewModel: ObservableObject { private let pixelReporter: OnboardingIntroPixelReporting private let onboardingManager: OnboardingHighlightsManaging + private let isIpad: Bool private let urlOpener: URLOpener init( pixelReporter: OnboardingIntroPixelReporting, onboardingManager: OnboardingHighlightsManaging = OnboardingManager(), + isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, urlOpener: URLOpener = UIApplication.shared ) { self.pixelReporter = pixelReporter self.onboardingManager = onboardingManager + self.isIpad = isIpad self.urlOpener = urlOpener - introSteps = onboardingManager.isOnboardingHighlightsEnabled ? OnboardingIntroStep.highlightsFlow : OnboardingIntroStep.defaultFlow + introSteps = if onboardingManager.isOnboardingHighlightsEnabled { + isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsIPhoneFlow + } else { + OnboardingIntroStep.defaultFlow + } } func onAppear() { @@ -66,7 +73,11 @@ final class OnboardingIntroViewModel: ObservableObject { } func appIconPickerContinueAction() { - state = makeViewState(for: .addressBarPositionSelection) + if isIpad { + onCompletingOnboardingIntro?() + } else { + state = makeViewState(for: .addressBarPositionSelection) + } } func selectAddressBarPositionAction() { @@ -126,5 +137,6 @@ private enum OnboardingIntroStep { case addressBarPositionSelection static let defaultFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison] - static let highlightsFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] + static let highlightsIPhoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] + static let highlightsIPadFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection] } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift new file mode 100644 index 0000000000..5ef61b133c --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift @@ -0,0 +1,79 @@ +// +// OnboardingView+AddressBarPositionContent.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI +import Onboarding + +private enum Metrics { + static let titleFont = Font.system(size: 20, weight: .semibold) + static let messageFont = Font.system(size: 16) +} + +extension OnboardingView { + + struct AddressBarPositionContentState { + var animateTitle = true + var showContent = false + } + + struct AddressBarPositionContent: View { + + private var animateTitle: Binding + private var showContent: Binding + private let action: () -> Void + + init( + animateTitle: Binding = .constant(true), + showContent: Binding = .constant(true), + action: @escaping () -> Void + ) { + self.animateTitle = animateTitle + self.showContent = showContent + self.action = action + } + + var body: some View { + VStack(spacing: 16.0) { + AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AddressBarPosition.title, startAnimating: animateTitle) { + showContent.wrappedValue = true + } + .foregroundColor(.primary) + .font(Metrics.titleFont) + + VStack(spacing: 24) { + OnboardingAddressBarPositionPicker() + + Button(action: action) { + Text(verbatim: UserText.HighlightsOnboardingExperiment.AddressBarPosition.cta) + } + .buttonStyle(PrimaryButtonStyle()) + } + .visibility(showContent.wrappedValue ? .visible : .invisible) + } + } + } + +} + +// MARK: - Preview + +#Preview { + OnboardingView.AddressBarPositionContent(action: {}) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 2f8f04508c..4cfba762c8 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -40,6 +40,7 @@ struct OnboardingView: View { @State private var animateComparisonText = false @State private var appIconPickerContentState = AppIconPickerContentState() + @State private var addressBarPositionContentState = AddressBarPositionContentState() init(model: OnboardingIntroViewModel) { self.model = model @@ -156,15 +157,11 @@ struct OnboardingView: View { } private var addressBarPreferenceSelectionView: some View { - // TODO: Implement View - VStack(spacing: 30) { - Text(verbatim: "Choose Address Bar Position") - - Button(action: model.selectAddressBarPositionAction) { - Text(verbatim: "Next") - } - .buttonStyle(PrimaryButtonStyle()) - } + AddressBarPositionContent( + animateTitle: $addressBarPositionContentState.animateTitle, + showContent: $addressBarPositionContentState.showContent, + action: model.selectAddressBarPositionAction + ) .onboardingDaxDialogStyle() } diff --git a/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift b/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift new file mode 100644 index 0000000000..5708195ae5 --- /dev/null +++ b/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift @@ -0,0 +1,93 @@ +// +// OnboardingAddressBarPositionPickerViewModelTests.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 OnboardingAddressBarPositionPickerViewModelTests: XCTestCase { + private var addressBarPositionManagerMock: AddressBarPositionManagerMock! + + override func setUpWithError() throws { + addressBarPositionManagerMock = AddressBarPositionManagerMock() + try super.setUpWithError() + } + + override func tearDownWithError() throws { + addressBarPositionManagerMock = nil + try super.tearDownWithError() + } + + func testWhenInitThenDisplayModelsAreCorrect() throws { + // GIVEN + addressBarPositionManagerMock.currentAddressBarPosition = .top + let sut = OnboardingAddressBarPositionPickerViewModel(addressBarPositionManager: addressBarPositionManagerMock) + + // WHEN + let items = sut.items + + // 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.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.icon, .addressBarBottom) + XCTAssertFalse(secondItem.isSelected) + } + + func testWhenUpdateAddressBarThenDisplayModelsAreUpdated() throws { + // GIVEN + addressBarPositionManagerMock.currentAddressBarPosition = .top + let sut = OnboardingAddressBarPositionPickerViewModel(addressBarPositionManager: addressBarPositionManagerMock) + XCTAssertEqual(sut.items.first?.type, .top) + XCTAssertTrue(sut.items.first?.isSelected ?? false) + + // WHEN + sut.setAddressBar(position: .bottom) + + // THEN + XCTAssertEqual(addressBarPositionManagerMock.currentAddressBarPosition, .bottom) + + 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.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.icon, .addressBarBottom) + XCTAssertTrue(secondItem.isSelected) + } + +} + +private class AddressBarPositionManagerMock: AddressBarPositionManaging { + var currentAddressBarPosition: AddressBarPosition = .top +} diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 0d12240db5..500ecb0ff9 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -104,7 +104,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -117,12 +117,14 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(didCallOnCompletingOnboardingIntro) } - // MARK: - Highlights State + Actions + // MARK: - Highlights State + Actions iPhone + + // MARK: iPhone - func testWhenSubscribeToViewStateAndIsHighlightsFlowThenShouldSendLanding() { + func testWhenSubscribeToViewStateAndIsHighlightsIphoneFlowThenShouldSendLanding() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) // WHEN let result = sut.state @@ -131,10 +133,10 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(result, .landing) } - func testWhenOnAppearIsCalledAndIsHighlightsFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + func testWhenOnAppearIsCalledAndAndIsHighlightsIphoneFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -144,10 +146,10 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } - func testWhenStartOnboardingActionIsCalledAndIsHighlightsFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { + func testWhenStartOnboardingActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) XCTAssertEqual(sut.state, .landing) // WHEN @@ -157,10 +159,10 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 3)))) } - func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -170,10 +172,10 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) } - func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -183,10 +185,10 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) } - func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { + func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) XCTAssertEqual(sut.state, .landing) // WHEN @@ -196,11 +198,11 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAddressBarPositionDialog, step: .init(currentStep: 3, totalSteps: 3)))) } - func testWhenSelectAddressBarPositionActionIsCalledAndIsHighlightsFlowThenOnCompletingOnboardingIntroIsCalled() { + func testWhenSelectAddressBarPositionActionIsCalledAndIsHighlightsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -213,6 +215,89 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(didCallOnCompletingOnboardingIntro) } + // MARK: iPad + + func testWhenSubscribeToViewStateAndIsHighlightsIpadFlowThenShouldSendLanding() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.state + + // THEN + XCTAssertEqual(result, .landing) + } + + func testWhenOnAppearIsCalledAndAndIsHighlightsIpadFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) + } + // + func testWhenStartOnboardingActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 2)))) + } + + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 2)))) + } + + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.cancelSetDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 2)))) + } + + func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + // MARK: - Pixels func testWhenOnAppearIsCalledThenPixelReporterTrackOnboardingIntroImpression() { From e4e52c2af79a005fd23ef9ad937d9bcadbe9b3d7 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 12 Sep 2024 10:02:35 +1000 Subject: [PATCH 21/31] Alessandro/onboarding copy and private search options (#3349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1208241563982253 **Description**: 1. Remove anonymous searches Option 3 for Highlights experiment. 2. Update anonymous search Surprise Me to search for “baby ducklings” for both US and International. 3. Update copy throughout the App for Highlights experiment. --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++ .../BrowsersComparisonModel.swift | 11 ++ .../ContextualOnboardingDialogs.swift | 18 +-- .../NewTabDaxDialogFactory.swift | 14 +- .../ContextualDaxDialogsFactory.swift | 13 +- .../OnboardingIntroViewModel+Copy.swift | 52 +++++++ .../OnboardingIntroViewModel.swift | 3 + ...ardingView+BrowsersComparisonContent.swift | 12 +- .../OnboardingView+IntroDialogContent.swift | 6 +- .../OnboardingIntro/OnboardingView.swift | 7 +- .../OnboardingSuggestedSearchesProvider.swift | 37 +++-- .../BrowserComparisonModelTests.swift | 146 ++++++++++++++++++ .../OnboardingIntroViewModelTests.swift | 58 ++++++- DuckDuckGoTests/OnboardingManagerMock.swift | 25 +++ ...ardingSuggestedSearchesProviderTests.swift | 64 +++++++- 15 files changed, 436 insertions(+), 42 deletions(-) create mode 100644 DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift create mode 100644 DuckDuckGoTests/BrowserComparisonModelTests.swift create mode 100644 DuckDuckGoTests/OnboardingManagerMock.swift 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 { From 76204ad206a3dfb654d7bf7b7d7df5024c638646 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 12 Sep 2024 11:15:46 +1000 Subject: [PATCH 22/31] Update Onboarding gradients (#3350) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208084960726981 **Description**: Update Onboarding Gradients --- DuckDuckGo.xcodeproj/project.pbxproj | 37 ++++++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../OnboardingBackground.swift | 19 ++++- .../Background/OnboardingGradient.swift | 79 +++++++++++++++++++ .../NewTabDaxDialogFactory.swift | 11 ++- .../ContextualDaxDialogsFactory.swift | 6 +- .../OnboardingIntroViewModel.swift | 3 + .../OnboardingIntro/OnboardingView.swift | 2 +- .../Styles/DaxDialogStyles.swift | 41 ++++++++-- 9 files changed, 174 insertions(+), 28 deletions(-) rename DuckDuckGo/OnboardingExperiment/{ => Background}/OnboardingBackground.swift (78%) create mode 100644 DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4e1068630..7f7abe48dc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -703,6 +703,8 @@ 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */; }; 9F8007262C5261AF003EDAF4 /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */; }; 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; + 9F96F73B2C9144D5009E45D5 /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 9F96F73A2C9144D5009E45D5 /* Onboarding */; }; + 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F96F73E2C914C57009E45D5 /* OnboardingGradient.swift */; }; 9F9A922E2C86A56B001D036D /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */; }; 9F9A92312C86AAE9001D036D /* OnboardingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */; }; 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92332C86B42B001D036D /* AppIconPicker.swift */; }; @@ -714,17 +716,16 @@ 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */; }; 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */; }; 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */; }; - 9FB893F82C784A1700332E5E /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 9FB893F72C784A1700332E5E /* Onboarding */; }; 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */; }; 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */; }; 9FDEC7B42C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */; }; - 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 */; }; + 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 */; }; 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 */; }; @@ -2494,6 +2495,7 @@ 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModelTests.swift; sourceTree = ""; }; 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddressBarPositionContent.swift"; sourceTree = ""; }; 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyDataReporter.swift; sourceTree = ""; }; + 9F96F73E2C914C57009E45D5 /* OnboardingGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingGradient.swift; sourceTree = ""; }; 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDebugView.swift; sourceTree = ""; }; 9F9A92332C86B42B001D036D /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; @@ -2509,12 +2511,12 @@ 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandlerTests.swift; sourceTree = ""; }; 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPickerViewModelTests.swift; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -3017,7 +3019,7 @@ F42D541D29DCA40B004C4FF1 /* DesignResourcesKit in Frameworks */, 85875B6129912A9900115F05 /* SyncUI in Frameworks */, F4D7F634298C00C3006C3AE9 /* FindInPageIOSJSSupport in Frameworks */, - 9FB893F82C784A1700332E5E /* Onboarding in Frameworks */, + 9F96F73B2C9144D5009E45D5 /* Onboarding in Frameworks */, 85D598872927F84C00FA3B1B /* Crashes in Frameworks */, D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */, ); @@ -4729,6 +4731,15 @@ path = ContextualOnboarding; sourceTree = ""; }; + 9F96F73D2C914C3D009E45D5 /* Background */ = { + isa = PBXGroup; + children = ( + 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, + 9F96F73E2C914C57009E45D5 /* OnboardingGradient.swift */, + ); + path = Background; + sourceTree = ""; + }; 9F9A922C2C86A560001D036D /* Manager */ = { isa = PBXGroup; children = ( @@ -4812,6 +4823,7 @@ 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */ = { isa = PBXGroup; children = ( + 9F96F73D2C914C3D009E45D5 /* Background */, 9FDEC7BD2C9125EC00C7A692 /* AddressBarPositionPicker */, 9F9A92322C86B419001D036D /* AppIconPicker */, 9F9A922C2C86A560001D036D /* Manager */, @@ -4822,7 +4834,6 @@ 9FB027172C26BC0F009EA190 /* BrowsersComparison */, 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, 9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */, - 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */, ); path = OnboardingExperiment; @@ -6444,7 +6455,7 @@ CB941A6D2B96AB08000F9E7A /* PrivacyDashboard */, F1D43AF92B99C1D300BAB743 /* BareBonesBrowserKit */, 9F8FE9482BAE50E50071E372 /* Lottie */, - 9FB893F72C784A1700332E5E /* Onboarding */, + 9F96F73A2C9144D5009E45D5 /* Onboarding */, ); productName = DuckDuckGo; productReference = 84E341921E2F7EFB00BDBA6F /* DuckDuckGo.app */; @@ -7218,6 +7229,7 @@ BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, + 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */, 6FE1273D2C204C2500EB5724 /* FavoritesView.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, 1E24295E293F57FA00584836 /* LottieView.swift in Sources */, @@ -10841,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.0.1; + version = 193.0.3; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -11046,8 +11058,9 @@ package = 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */; productName = Lottie; }; - 9FB893F72C784A1700332E5E /* Onboarding */ = { + 9F96F73A2C9144D5009E45D5 /* Onboarding */ = { isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Onboarding; }; B6DFE6D52BC7E47F00A9CE59 /* SwiftLintTool */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fff3117345..ef85291655 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" : "dc04bd2707bd64e743961d88675606c0ea4539b1", - "version" : "193.0.1" + "revision" : "c68e68a0036796628ffdea8d449e5df39ceceb4d", + "version" : "193.0.3" } }, { diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift b/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift similarity index 78% rename from DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift rename to DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift index 716c0b210b..7e5b6fb531 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift +++ b/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift @@ -18,9 +18,9 @@ // import SwiftUI -import Onboarding 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 +35,7 @@ struct OnboardingBackground: View { .opacity(colorScheme == .light ? 0.5 : 0.3) .frame(width: proxy.size.width, height: proxy.size.height, alignment: alignment) .background( - OnboardingGradient() + OnboardingGradientView(type: gradientType) .ignoresSafeArea() ) } @@ -46,13 +46,26 @@ private enum Metrics { static let imageCentering = MetricBuilder(iPhone: .bottomLeading, iPad: .center) } - #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 new file mode 100644 index 0000000000..c5b6fff5a6 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift @@ -0,0 +1,79 @@ +// +// OnboardingGradient.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import 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 + OnboardingGradient() + @unknown default: + linearLightGradient + } + } + + 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/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index b4ff585bc7..7f6f068844 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -32,6 +32,10 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { private let onboardingPixelReporter: OnboardingPixelReporting private let onboardingManager: OnboardingHighlightsManaging + private var gradientType: OnboardingGradientType { + onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + } + init( delegate: OnboardingNavigationDelegate?, contextualOnboardingLogic: ContextualOnboardingLogic, @@ -57,7 +61,6 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { createFinalDialog(onDismiss: onDismiss) default: EmptyView() - } } @@ -68,7 +71,7 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { OnboardingTrySearchDialog(message: message, viewModel: viewModel) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .onFirstAppear { [weak self] in self?.onboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTrySearchUnique) } @@ -80,7 +83,7 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { OnboardingTryVisitingSiteDialog(logoPosition: .top, viewModel: viewModel) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .onFirstAppear { [weak self] in self?.onboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTryVisitSiteUnique) } @@ -104,7 +107,7 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { }) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .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 570d4dc93e..04fa68f5df 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -50,6 +50,10 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { private let contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding private let onboardingManager: OnboardingHighlightsManaging + private var gradientType: OnboardingGradientType { + onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + } + init( contextualOnboardingLogic: ContextualOnboardingLogic, contextualOnboardingSettings: ContextualOnboardingSettings = DefaultDaxDialogsSettings(), @@ -95,7 +99,7 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { let viewWithBackground = rootView .onboardingDaxDialogStyle() - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .gradientOnly(gradientType)) let hostingController = UIHostingController(rootView: AnyView(viewWithBackground)) if #available(iOS 16.0, *) { hostingController.sizingOptions = [.intrinsicContentSize] diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index 8724b06879..419ea7daf1 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -19,12 +19,14 @@ import Foundation import Core +import Onboarding import class UIKit.UIApplication 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] @@ -50,6 +52,7 @@ final class OnboardingIntroViewModel: ObservableObject { } copy = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + gradientType = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default } func onAppear() { diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 64a4d56622..cd834e55b2 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -19,7 +19,6 @@ import SwiftUI import Onboarding -import struct DuckUI.PrimaryButtonStyle // MARK: - OnboardingView @@ -57,6 +56,7 @@ struct OnboardingView: View { onboardingDialogView(state: viewState) } } + .onboardingGradient(model.gradientType) } private func onboardingDialogView(state: ViewState.Intro) -> some View { diff --git a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift index ea123baec5..881e5fd721 100644 --- a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift +++ b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift @@ -33,16 +33,23 @@ extension OnboardingStyles { } struct BackgroundStyle: ViewModifier { + let backgroundType: OnboardingBackgroundType func body(content: Content) -> some View { ZStack { - OnboardingBackground() - .ignoresSafeArea(.keyboard) + switch backgroundType { + case let .illustratedGradient(gradientType): + OnboardingBackground() + .onboardingGradient(gradientType) + .ignoresSafeArea(.keyboard) + case let .gradientOnly(gradientType): + OnboardingGradientView(type: gradientType) + .ignoresSafeArea(.keyboard) + } content } } - } } @@ -57,8 +64,32 @@ extension View { modifier(OnboardingStyles.DaxDialogStyle()) } - func onboardingContextualBackgroundStyle() -> some View { - modifier(OnboardingStyles.BackgroundStyle()) + func onboardingContextualBackgroundStyle(background: OnboardingBackgroundType) -> some View { + modifier(OnboardingStyles.BackgroundStyle(backgroundType: background)) } } + +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) + } + +} From 966fa4931cd08f3fe49dd4895e7f4739581ad077 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 12 Sep 2024 11:10:35 +0200 Subject: [PATCH 23/31] C.S.S Bump (Via BSK) (#3346) Task/Issue URL: https://app.asana.com/0/1204099484721401/1208271424696738/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/983 **Description**: - Updates C.S.S - Adds subfeature flag --- Core/FeatureFlag.swift | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- .../DuckPlayer/Resources/DuckPlayer-ModalAnimation.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 229189a0a1..763b282091 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -78,7 +78,7 @@ extension FeatureFlag: FeatureFlagSourceProviding { case .newTabPageSections: return .remoteDevelopment(.feature(.newTabPageImprovements)) case .duckPlayer: - return .remoteReleasable(.feature(.duckPlayer)) + return .remoteReleasable(.subfeature(DuckPlayerSubfeature.enableDuckPlayer)) case .sslCertificatesBypass: return .remoteReleasable(.subfeature(SslCertificatesSubfeature.allowBypass)) case .syncPromotionBookmarks: diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7f7abe48dc..a270aad407 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.0.3; + version = 193.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ef85291655..3c522a6bbf 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" : "c68e68a0036796628ffdea8d449e5df39ceceb4d", - "version" : "193.0.3" + "revision" : "b55c174793e630d6ec4e0ef471f32a557697cc21", + "version" : "193.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "b8c858a33cfe665ddfe177df4dbd63d12f6777a6", - "version" : "6.14.1" + "revision" : "9f3717b3913a12956f1386fe7b657f68545fba83", + "version" : "6.15.0" } }, { diff --git a/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json b/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json index a4ab771677..a334e04618 100644 --- a/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json +++ b/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json @@ -1 +1 @@ -{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":7.00000028511585,"op":368.000014988947,"w":320,"h":180,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":20,"h":20,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABRklEQVQ4jbXVMWuDQBQH8P/TcwilBFKSpWRJHZ0zmTF7+jW6Cv0QBdd8jWbvGKfMjsYldEmoICVk8MzrUC1yJ9Yq/Y/Pdz/Ow3cSlDDzEsAjgJn6TEkM4JWI3qpFqkAPALwWUB3sE9H+ByywFwA3f8TKnAE8E9HeKApeDwzFWg8ARHFm2msmKQ+CMLcPp+skkxAAYAnI6dg4uo4ZjYZ0UZbMmHlJzLxWwfcPvt1ss3kJqbEE5Gph7e7v6FN5FBsqlqQ8aMIAIJMQm202T1IeqLs01OYgzO0mrIoGYW6rdQ08nK6T37CmXg1ss7umXg3sGw20BGTbxXW9GjgdG8e2YF2vBrqOGbXZpSUgXceM6sC4WhgN6bJaWLsmtPywa6YlpmL0PHVRh9EDAL+8bbTx65CYiJ7KM/TxfQV1zbkw/umCrabvL+AL4d2TwJBKfRgAAAAASUVORK5CYII=","e":1},{"id":"image_1","w":42,"h":9,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAJCAYAAABE+77DAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAbUlEQVQ4je3ToQ3CUBRA0fO+ZYDimYIBmIiEWViCBN8tOgC6A/Trh8I1FEHyEJwJrrn8fVdAZg4YiltWRcQEkZknnIt73nngEpl5w666ZsPY/H4k7Bt6dcUH5oZrdcWGjvvr+gOOtT2rOsaIWJ6fBxfBNPVVKAAAAABJRU5ErkJggg==","e":1},{"id":"image_2","w":67,"h":17,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAAARCAYAAACGjBGPAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAACkUlEQVRYheWXz0tUURTHP+f1hplnjtaUkyGpyVhCNBCBoeCyRatsU7uCVlKLFpK08R+QoEUgErQtWtWuRYtsYxHhYANJ+COkFJxq0BydkRnnuph5M883o74RfaJ9YODec+65833nvXvuvYINpVQYuAGEgaN2/yEgBowAr0Vk3uoQs6GUqgZ6gQ53te0by8CQiLwzDQKFRAwALSUhC8OBQtvXmMTXktxzme7yRkSGAPS8oRd7ImYHm4i9DJFN6hvstV1zNPePowcybih1gW6l1FcRGZF8jRjY4P7R30b8bdOm4d4zS7Q9/3yIEhITkds6uWJZZGE4sGUiAFZ/+pl6dJHzzyI2jw/w7qrMMv8OpKyG5KrSIxNrDatp8WwX7PWo9KXWI7OGV6wvMqiU6tTJ7RpF5l9snQiTxGiQ1LRhqSG1+Z8b/AaSkEvEq/eZ9oWE8jsNjkxmQ3eveT7YEtKhYd8+V8YDOCURtQpwLGYXOG42IhNrDZUkAiCdQf/0LdtsM9drJSPtBXMrVr7XWHqlc+0dBY1OloZTSh/AE3C+dda0xy09N4tpQWPwmPzbyQQ11VLynBq5E1kR/5WYfVBZNCNDddgq5A+Q3YmwCskAi2bnwlkt1tYoM5VM0HJam7vcqs3azGOilOoBugum1LTB+J3ObZdL3c0pGvsmy3h8lQjbAalyxviiMv4mssZ2wVU+Ld1wQpbKuO6LUuoUMIi1kCZG/Uw8aN80IbVdc4SeRB1JPxhEReSheRy/Su4UWiQ1bfDraYilL8FCUqrOxam7NcPJ686W0sFgGbgnIvPWi9rG5fJ/sAz0icgUWG6tAEqpTqAHCO6DMLeJAo+t13gpNyq/bMJAvUvC3GQM+Gh+DVbWAVNnztan4+NpAAAAAElFTkSuQmCC","e":1},{"id":"image_3","w":141,"h":49,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_4","w":29,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAECAYAAABySjRcAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAMElEQVQYlb3NQREAIAwEsYABrFQKUpBeB4eL7sy+I1TokKlXaByDbbxJEBaEi5pCP6MOPNpI69kPAAAAAElFTkSuQmCC","e":1},{"id":"image_5","w":101,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGUAAAAECAYAAACX1PEwAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAQUlEQVQ4jWO8e/fuf4ZRMCgAIyPjQiUlpQTG0UgZdGAi00C7YBSggI9MTEwLWBgYGBoH2iWjAAKYmJg2KCoqXgAApAUNMclgLKQAAAAASUVORK5CYII=","e":1},{"id":"image_6","w":141,"h":87,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_7","w":141,"h":18,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAASCAYAAABmbl0zAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABd0lEQVRoge3Zv2rCUBQG8C/RJYJUcAoKLQTrJqWpD+FQcO0TdMvkkzjqSxQKOlq3Ll1utuwODtdFMbjkdupiE//de5pIz29MyJcD+cjlJla3231IkuQNwC0YO0ApNU+SpF9yXfcTXBh2Asuy7mzbdss4UBh/vT47+Kta1ZmLFV+/nHVmFEV4vKA0kePgtd3GulTSmowV1o2ddvRZyosKAwD3cYyX5VJrKlZsqW8ad7fTCn3abDDaO9ZqtbQy90kpsVqtjGay02QuT6ZUKhUEQYBGo2E8ezqdYjKZGM9lh6UuTyb1ej2SwvxkN5tNkmyWjbw01A+10+mQ5rPfyEtDTUqZ9wj/DnlpZrMZWXYcxwjDkCyfpSMvjRAC4/EYi8XCaG4YhhgOh9hut0Zz2XEku6f9D3tCCAghKG7FcpD6ptH9FfBer2tdz4otszQDz0PkOGeFRY6Dgefho1YzMhwrJsv3fZX3EOy6XP2Wm/29slJqnvcQ7Lp8A7ghXrH1RxAYAAAAAElFTkSuQmCC","e":1},{"id":"image_8","w":405,"h":708,"u":"","p":"data:image/jpeg;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"Channel.pdf","cl":"pdf","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[220.75,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[220.75,47.75,0]}],"ix":2},"a":{"a":0,"k":[10,10,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":2,"nm":"Text Ad.pdf","cl":"pdf","refId":"image_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[120.75,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[120.75,43.75,0]}],"ix":2},"a":{"a":0,"k":[21,4.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":2,"nm":"Ad.pdf","cl":"pdf","refId":"image_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[130.5,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[130.5,95.875,0]}],"ix":2},"a":{"a":0,"k":[33.5,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"Description.pdf","cl":"pdf","refId":"image_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[161.417,143,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":64,"s":[162.917,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.917,204,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[161.417,143,0]}],"ix":2},"a":{"a":0,"k":[70.5,24.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"Line 9.pdf","cl":"pdf","parent":7,"refId":"image_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":64,"s":[20.208,79.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":76,"s":[20.208,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":92,"s":[20.208,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":341,"s":[20.208,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"t":355.000014459446,"s":[20.208,79.125,0]}],"ix":2},"a":{"a":0,"k":[14.5,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[1,0.800000071526,0.200000017881,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[0.93412989378,0.700597524643,0,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.951],"y":[0.71]},"t":47,"s":[100]},{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":341,"s":[0]},{"t":355.000014459446,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"Line 10.pdf","cl":"pdf","parent":7,"refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":64,"s":[85.708,79.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":76,"s":[85.708,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":92,"s":[85.708,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":341,"s":[85.708,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"t":355.000014459446,"s":[85.708,79.125,0]}],"ix":2},"a":{"a":0,"k":[50.5,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":64,"s":[101.485,100,100]},{"t":92.0000037472368,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"Duck PLayer.pdf","cl":"pdf","refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[162.917,75.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":64,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":92,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":341,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"t":355.000014459446,"s":[162.917,75.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,43.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[0.978,0.978,1]},"o":{"x":[0.055,0.055,0.333],"y":[0.409,0.409,0]},"t":99,"s":[100,100,100]},{"i":{"x":[0.348,0.348,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":123,"s":[167.816,167.816,100]},{"i":{"x":[0.858,0.858,0.667],"y":[1.003,1.003,1]},"o":{"x":[0.527,0.527,0.167],"y":[0.386,0.386,0]},"t":341,"s":[167.816,167.816,100]},{"t":355.000014459446,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0,0],[0,87],[141,87],[141,0]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":2,"nm":"Top Bar.pdf","cl":"pdf","refId":"image_7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":346,"s":[3]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[163.042,23.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":346,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[163.042,23.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,9,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":1,"nm":"Black Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.016]},"o":{"x":[0.077],"y":[0.68]},"t":99,"s":[9]},{"i":{"x":[0],"y":[8.191]},"o":{"x":[0.167],"y":[0]},"t":123,"s":[59]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":341,"s":[59]},{"t":355.000014459446,"s":[9]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160.5,88,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[105,108.889,100],"ix":6}},"ao":0,"sw":320,"sh":180,"sc":"#000000","ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":2,"nm":"Rectangle.png","cl":"png","refId":"image_8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.038]},"o":{"x":[0.027],"y":[1.042]},"t":99,"s":[0]},{"i":{"x":[0],"y":[35.719]},"o":{"x":[0.167],"y":[0]},"t":123,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":341,"s":[100]},{"t":355.000014459446,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[132.5,-168.5,0],"ix":2},"a":{"a":0,"k":[202.5,354,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":55.0000022401959,"op":955.000038897947,"st":55.0000022401959,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":368.000014988947,"w":320,"h":180,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":20,"h":20,"u":"","p":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSgBBwcHCggKEwoKEygaFhooKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKP/AABEIABQAFAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APprWtVg0i08+4ySTtRF6sa3w+HlXlyxIqVFBXZh6X4zt7u7SC4t2tw52q+/cM+/AxXZWyydOPNF3sYwxKk7NWOrrzDpOU+IGnz3dnbz26NIIC29VGTg45x7Y/WvTyytGnNxlpc5sTBySa6HE6Vp1xqN4kFujElhubHCD1Jr2a1aFGDlJnJCDm7I9hr5M9QKACgAoA//2Q==","e":1},{"id":"image_1","w":42,"h":6,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAGCAYAAAC1rQwWAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAKElEQVQokWP8////AYbBDy6wMDAw2A+0K4gBTAPtAGLBqEOpDYaMQwFnXQTaXe9dvQAAAABJRU5ErkJggg==","e":1},{"id":"image_2","w":67,"h":17,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAAARCAYAAACGjBGPAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABsUlEQVRYhe2WPS9EQRSGnxl31374WB+7IkE0ZEVIhEoiOoVKI9lo/AR+gl+gkvgNCoVGJWELjQaJRISCsGJ9rYRgl3tHoSFzd+9dca8NnvLMOZN33jlnMkIptcE/ADsGMPrTKioF+dMCKgmj6Ir5BFdLkFuDl2sQQagZgEQKoj0+SvQPoZRSWvT5BI5mIH9mUyKhbRZaprxX5y9pvTPMpxJGAFhwNg+BZmgc81ZeGby8KnIPCpur/URDjSAYELZruhk3KyWM+EBmoWLM2D402dwzeTWdc6WAoaRkuFc/uv6A5tbdKSicw+OBu1wPubyzSO+6MwLAUrC1b3F8YWlruhn5U/dKCln3uR6RvXWYiyJc2NTpZhgx9zvK0JeEfCfRkP38O9fpMd2M2kF3u4kgRPu+JOQ76WwVtMfLMyQREyQ7bPpAz0zB1TKoQukd45NQFS5LhBdIIZgYMTjKWOTunfPrItDVJgkYuoH2/4ybVTieA/RHBoBoP3QvgqwuT3llk7b/jjeNvx82kvwclxFomf6NRgDFOuMj+cz7j9Soh3A3yIBP0nwn7WzG36HImPxR3gAgSnf+tTIOIgAAAABJRU5ErkJggg==","e":1},{"id":"image_3","w":141,"h":49,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAAxCAYAAADnViqrAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAE6ElEQVR4nO2cTW8jNRjH/36ZSSZJ36sWVKkFLQtCohIcVkhw4MQNiQNfAS58BC6c+AYc+ATcEReEhFhx4bSIwyLx0iKBtIt2K9Ldtklmxh6bwzJJJnGaTJrMOIl/t9jJ+Innbz+PH4+HnJyc3IXDkQNOCHmnbCMciwUt2wDH4sFNhYQQcM7BGAMhpGibHCWjlEKSJJBSGuuHRMMYg+/7TiwrDGMMjDF4nocoiqCUytRnREMpHSkYologOjI2okgNoNUZmu2wAUIIKpUKOp1OpjwjGs/zsoLRErWrr1FrfwMuH4y8uAaB8F5Ga+0DRMFbs7XcUSqEEHieByFEr+z09FSnlUEQ9L6tQmz/+yn8+JdcjbTq7+Ny86Oh8nTKs83taa0hpRyagh09tNaZ2aY70wzezPWnX+QWDADUW19Bei+gU3+3W+Z5HjzPm8beQmCMGX23DYQxwXmLQ+n5tuMxjZ01CWZYTw9qwygaKh8jaH83tQGNyy8zouHcuEizhnS1GMdx2aZ0aYUU9/6s4WHTA1DM7Myoxu3nQxwfdYziSTFWVaKfQDC9tFlyBi7+mvr3q85lh+Lbn9fxsOmjKMEAQKIIfn0Q4O79NSTXTLpG0bCkeWMDmHzUMyZJbny9eWOTjT/+1kAky8u7nl14uP93MLK+EL+RTvuMsSKay0UaCNsimsdPOZpX5bvzP/6p4viwA2rQbmHW2RQv2MzZRfmCAQCZEJy3GHbWhgeT23uyDK3tSUnEI1xkobKmprlugdFaQ+vZroWrvj3L/rXA7LILEU2aVbQtsTcLZh0PHWzHuEdqpc84Ww2JRtUsYOPQ1+TmiThNe9H3sgoGmH1wH/garx6EM71mfjTeeLE9stYoGsmPbtgkgeCH3c/LKhjg2X+btds9Purg1nPlCIcSjTdvt7C3YX4sAhjhnqLq60joNpiaLl8TVe9As43uZ6XU0sUzKVrrmW8/EALceamNw90Yp48qaF5ySDXfgVfhCnsbEq8chCPdUkpXNJmAjvi42PwYm83PcmeGFanhcuPDTJmUcildVJrjmRf7mxL7m/O7/qQMBvvdXW4ACIIgc2Or7R+w/uRzUD3av/Uj2T6ebH8C6d8y1hNClkY481g52UqSJIii3rNUGdGYdqOJaiFofw8v/h1EXxkvqtgOYv81hMHbALEjOeWYHVEUZVaImTsshABjLBN/aFpHu/FecRY6rMKUUhiKTsMwnKufdiwGWmsIIYzbP0ZfEscxhBCglC5NDOKYHK31tQnLkQHIuB86VpflTJ445ooTjSM3TjSO3DjROHLjROPIzcTpW0rpUOJvlRh3qC49BlNk/6SbpUXn1SYSDed8KTcc88I5hxAic0QVeDagKpVKaf3DOUcURYXthY0dFpRSJ5g+PM8bmk3KFAzQe3FDYe2N+wLn3AlmgP4To7acTy/SjrGisaFDbKN/prEpxivKlrGtrMozI3noD4ZtemlAUbaMFY2U0glngP7VSpIkVvRPkXaMFY1SCkIIKzrGBoQQQyO6yJWLCaVUoSdYJ1pyp/mJVQ6Kr8vTKKUQhqHL0wxStJoXjfShpVXAntDfsTA40ThyQ23KMzgWA5p5o6fDMQF0a2trZVdEjumgvu9jb2/PCccxEY1GA0T/n5WK4xjn5+fodDpWpcYddkAIQb1ex+7uLv4DTsP3dx/5jJUAAAAASUVORK5CYII=","e":1},{"id":"image_4","w":141,"h":18,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAASCAYAAABmbl0zAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABzUlEQVRoge3YsUsCURwH8K+VHppGmBWXKIIQt4Z/gBwt0n/R0ugf4F8gTq5Jg6JTszlGuAUORdtNguDkkUle3B3na2qonmH6Xmr8Ptvdgy+/4cu9987HGLurVquoVCqwbRuE8ESjURQKBei6Dl+9XmflcnnZM5E1EAgEUCwW4dN1nY1Go2XPQ9ZELBbD1rTCHFsWLvp9HLruzIEvm5u4UlU8RCKiZiQrZjAYYIu3cOA4uDQMRDzv16Enr6841zQYodDCA5LVtMF7efr8PFdhAEBhDGemudBQZLVxvzTzFuZDaDL59JxKpZDL5RARuG11u120Wi2Mx2NhmWQ23NKIlEgkkM/n4ff7heYmk0mk02mUSiVMvpSUyMXdnkTKZrPCC/MhHo9D0zQp2WQ66aUJBoNS88PhsNR88p300hiGIS3b8zyp+YRPemna7TY6nY7wXMdx0Gg0MBwOhWeTn0k/CDPGUKvV0Gw2oaqqkEzXddHr9WBZlpA88jvc0jwueE542t7+9s40TZj0/+Zf4G5P9zs7uN7fnyvwdncXN3t7Cw1FVpsvk8mwaYtHto3029vMYT1FQVfybYks349nmr6ioK8ofzULWRPSb0/k/3kHh4mHa1Tzd/gAAAAASUVORK5CYII=","e":1},{"id":"image_5","w":29,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAECAYAAABySjRcAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAMElEQVQYlb3NQREAIAwEsYABrFQKUpBeB4eL7sy+I1TokKlXaByDbbxJEBaEi5pCP6MOPNpI69kPAAAAAElFTkSuQmCC","e":1},{"id":"image_6","w":101,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGUAAAAECAYAAACX1PEwAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAQUlEQVQ4jWO8e/fuf4ZRMCgAIyPjQiUlpQTG0UgZdGAi00C7YBSggI9MTEwLWBgYGBoH2iWjAAKYmJg2KCoqXgAApAUNMclgLKQAAAAASUVORK5CYII=","e":1},{"id":"image_7","w":141,"h":87,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_8","w":405,"h":708,"u":"","p":"data:image/jpeg;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":1,"nm":"Pale Orange Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.075],"y":[0.996]},"t":135,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":139,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0.013]},"t":150,"s":[100]},{"t":151.000006150356,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,90,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.326,"y":1},"o":{"x":0.739,"y":0.739},"t":139,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-8.289,-1.382],[-2.125,-2.625],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.75,1.125],[0.957,1.182],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[146.375,71.5],[158.375,65.625],[169.875,72.5],[169.5,68.25]],"c":true}]},{"i":{"x":0.302,"y":0.302},"o":{"x":0.752,"y":0},"t":144,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-11.125,-1],[-0.75,4.375],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.816,0.613],[0.275,-1.606],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[145.125,76],[157.5,85.25],[171.125,76.5],[169.5,68.25]],"c":true}]},{"t":150.000006109625,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-8.289,-1.382],[-2.125,-2.625],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.75,1.125],[0.957,1.182],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[146.375,71.5],[158.375,65.625],[169.875,72.5],[169.5,68.25]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[0.65986520052,0.609302341938,0.562351107597,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[1,0.939583837986,0.883195459843,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":139,"s":[0]},{"t":144.00000586524,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"sw":320,"sh":180,"sc":"#d4beb0","ip":130.000005295009,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":161,"s":[100]},{"t":188.000007657397,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[134.5,59.25,0],"ix":2},"a":{"a":0,"k":[-14,-11,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,18.987]},"t":161,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":188,"s":[90,90,100]},{"t":297.000012097058,"s":[90,96.41,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":4,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":6,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":15.1,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964307598039,0.948043763404,0.948043763404,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-14,-10.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":58.0000023623884,"op":958.00003902014,"st":58.0000023623884,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":144,"s":[100]},{"t":173.000007046434,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[136,76.5,0],"ix":2},"a":{"a":0,"k":[-14,-11,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,18.987]},"t":144,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":173,"s":[90,90,100]},{"t":297.000012097058,"s":[90,96.41,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":4,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":6,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":15.1,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964307598039,0.948043763404,0.948043763404,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-14,-10.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41.0000016699642,"op":941.000038327716,"st":41.0000016699642,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"Channel.eps","cl":"eps","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[218.75,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[218.75,47.75,0]}],"ix":2},"a":{"a":0,"k":[10,10,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[5.523,0],[0,-5.523],[-5.523,0],[0,5.523]],"o":[[-5.523,0],[0,5.523],[5.523,0],[0,-5.523]],"v":[[10,0],[0,10],[10,20],[20,10]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"Text Ad.eps","cl":"eps","refId":"image_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[118.75,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[118.75,43.75,0]}],"ix":2},"a":{"a":0,"k":[21,3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[1.657,0],[0,0],[0,-1.657],[-1.657,0],[0,0],[0,1.657]],"o":[[0,0],[-1.657,0],[0,1.657],[0,0],[1.657,0],[0,-1.657]],"v":[[39,0],[3,0],[0,3],[3,6],[39,6],[42,3]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"Ad.eps","cl":"eps","refId":"image_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[132,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[132,95.875,0]}],"ix":2},"a":{"a":0,"k":[33.5,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[4.694,0],[0,0],[0,-4.694],[-4.694,0],[0,0],[0,4.694]],"o":[[0,0],[-4.694,0],[0,4.694],[0,0],[4.694,0],[0,-4.694]],"v":[[58.5,0],[8.5,0],[0,8.5],[8.5,17],[58.5,17],[67,8.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"Description","refId":"image_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[163.167,143,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":59,"s":[162.667,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.667,204,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[163.167,143,0]}],"ix":2},"a":{"a":0,"k":[70.5,24.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0.046,0],[0,0],[0,-0.035],[0,0],[-4.418,0],[0,0],[0,4.119],[0,0]],"o":[[0,0],[-0.035,0],[0,0],[0,4.418],[0,0],[4.119,0],[0,0],[0,-0.046]],"v":[[141.208,-0.5],[-0.104,-0.5],[-0.167,-0.438],[-0.167,41.216],[7.833,49.216],[133.833,49.216],[141.292,41.757],[141.292,-0.416]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":2,"nm":"Top Bar.eps","cl":"eps","refId":"image_4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[99]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[3]},{"t":358.000014581639,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":47,"s":[163.042,24.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[163.042,24.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,9,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[2.401,0],[0,0],[0,-2.209],[0,-2.557],[0,0],[0,2.402]],"o":[[0,0],[-2.209,0],[0,2.209],[0,0],[0,-3.466],[0,-2.402]],"v":[[136.039,0.5],[4.396,0.5],[0.396,4.5],[0.41,17.432],[140.311,17.432],[140.387,4.848]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":2,"nm":"Line 9.pdf","cl":"pdf","parent":12,"refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[6.107,80.125,0],"ix":2},"a":{"a":0,"k":[0.25,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":346,"s":[161,100,100]},{"t":360.000014663101,"s":[100,100,100]}],"ix":6}},"ao":0,"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[1,0.800000071526,0.200000017881,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[0.93412989378,0.700597524643,0,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.951],"y":[0.71]},"t":47,"s":[100]},{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":346,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":2,"nm":"Line 10.pdf","cl":"pdf","parent":12,"refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.838,80.125,0],"ix":2},"a":{"a":0,"k":[101.076,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":346,"s":[83,100,100]},{"t":360.000014663101,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":2,"nm":"Duck PLayer.eps","cl":"eps","refId":"image_7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[162.917,75.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":61,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":83,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":348,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[162.917,75.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,43.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[0.987,0.987,1]},"o":{"x":[0.055,0.055,0.333],"y":[0.238,0.238,0]},"t":69,"s":[100,100,100]},{"i":{"x":[0.348,0.348,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":83,"s":[167.816,167.816,100]},{"i":{"x":[0.858,0.858,0.667],"y":[1.002,1.002,1]},"o":{"x":[0.527,0.527,0.167],"y":[0.276,0.276,0]},"t":348,"s":[167.816,167.816,100]},{"t":358.000014581639,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[-0.074,0],[0,0],[0,-0.029],[0,0],[0.006,0],[0,0],[0,-0.007],[0,0]],"o":[[0,0],[-0.055,0],[0,0],[0,-0.006],[0,0],[-0.007,0],[0,0],[0,0.074]],"v":[[140.892,0.447],[0.333,0.447],[0.235,0.5],[0.235,85.625],[0.223,85.614],[140.771,85.614],[140.758,85.627],[140.758,0.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[1.105,0],[0,0],[0,-1.105],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-1.105]],"v":[[138.758,0.447],[2.235,0.447],[0.235,2.447],[0.235,83.614],[2.235,85.614],[138.758,85.614],[140.758,83.614],[140.758,2.447]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":348,"s":[{"i":[[1.105,0],[0,0],[0,-1.105],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-1.105]],"v":[[138.758,0.447],[2.235,0.447],[0.235,2.447],[0.235,83.614],[2.235,85.614],[138.758,85.614],[140.758,83.614],[140.758,2.447]],"c":true}]},{"t":358.000014581639,"s":[{"i":[[-0.074,0],[0,0],[0,-0.029],[0,0],[0.006,0],[0,0],[0,-0.007],[0,0]],"o":[[0,0],[-0.055,0],[0,0],[0,-0.006],[0,0],[-0.007,0],[0,0],[0,0.074]],"v":[[140.892,0.447],[0.333,0.447],[0.235,0.5],[0.235,85.625],[0.223,85.614],[140.771,85.614],[140.758,85.627],[140.758,0.312]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":1,"nm":"Dark Gray Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[7]},{"i":{"x":[0],"y":[1.014]},"o":{"x":[0.077],"y":[0.601]},"t":69,"s":[6]},{"i":{"x":[0],"y":[9.742]},"o":{"x":[0.167],"y":[0]},"t":83,"s":[39]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":348,"s":[40]},{"t":358.000014581639,"s":[7]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.125,87.75,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[105,108.889,100],"ix":6}},"ao":0,"sw":320,"sh":180,"sc":"#2d2d2d","ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":2,"nm":"Rectangle.png","cl":"png","refId":"image_8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.022]},"o":{"x":[0.027],"y":[0.608]},"t":69,"s":[0]},{"i":{"x":[0],"y":[43.204]},"o":{"x":[0.167],"y":[0]},"t":83,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":348,"s":[100]},{"t":358.000014581639,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[132.5,-168.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[132.5,-105.5,0]}],"ix":2},"a":{"a":0,"k":[202.5,354,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":55.0000022401959,"op":955.000038897947,"st":55.0000022401959,"bm":0}],"markers":[{"tm":142.192505791619,"cm":"1","dr":0},{"tm":147.000005987433,"cm":"2","dr":0},{"tm":156.00000635401,"cm":"3","dr":0}]} From 1ead3fc60be5cd2f17f260260b6ef66ea8d88bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Thu, 12 Sep 2024 17:59:59 +0200 Subject: [PATCH 24/31] Remove checking for negative attribution case (#3355) Task/Issue URL: https://app.asana.com/0/414709148257752/1208213972615184/f Tech Design URL: CC: Description: Removes the negative attribution pixel report, since the data was already collected. --- Core/PixelEvent.swift | 2 -- .../AdAttributionPixelReporter.swift | 24 +++++++++---------- .../AdAttributionPixelReporterTests.swift | 7 ++---- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index e5cb5bcdf9..2afe5960b8 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -725,7 +725,6 @@ extension Pixel { // MARK: Apple Ad Attribution case appleAdAttribution - case appleAdAttributionNotAttributed // MARK: Secure Vault case secureVaultL1KeyMigration @@ -1446,7 +1445,6 @@ extension Pixel.Event { // MARK: - Apple Ad Attribution case .appleAdAttribution: return "m_apple-ad-attribution" - case .appleAdAttributionNotAttributed: return "m_apple-ad-attribution_not-attributed" // MARK: - User behavior case .userBehaviorReloadTwiceWithin12Seconds: return "m_reload-twice-within-12-seconds" diff --git a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift index 24811b6fd0..a09eb9d693 100644 --- a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift +++ b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift @@ -22,7 +22,7 @@ import Core final actor AdAttributionPixelReporter { - static let isAdAttributionReportingEnabled = true + static let isAdAttributionReportingEnabled = false static var shared = AdAttributionPixelReporter() @@ -56,17 +56,17 @@ final actor AdAttributionPixelReporter { } if let (token, attributionData) = await self.attributionFetcher.fetch() { - let event: Pixel.Event = attributionData.attribution ? .appleAdAttribution : .appleAdAttributionNotAttributed - let parameters = attributionData.attribution ? self.pixelParametersForAttribution(attributionData, attributionToken: token) : [:] - - do { - try await pixelFiring.fire( - pixel: event, - withAdditionalParameters: parameters, - includedParameters: [.appVersion, .atb] - ) - } catch { - return false + if attributionData.attribution { + let parameters = self.pixelParametersForAttribution(attributionData, attributionToken: token) + do { + try await pixelFiring.fire( + pixel: .appleAdAttribution, + withAdditionalParameters: parameters, + includedParameters: [.appVersion, .atb] + ) + } catch { + return false + } } await fetcherStorage.markAttributionReportSuccessful() diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift index 1c6a2a6d7c..eb21f4507b 100644 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift @@ -124,16 +124,13 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertNil(pixelAttributes["ad_id"]) } - func testNotAttributedPixelFiredAndMarkedReported_WhenAttributionFalse() async throws { + func testPixelNotFiredAndMarksReport_WhenAttributionFalse() async { let sut = createSUT() attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: false)) let result = await sut.reportAttributionIfNeeded() - let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) - - XCTAssertEqual(pixelAttributes, [:]) - XCTAssertEqual(PixelFiringMock.lastPixel?.name, "m_apple-ad-attribution_not-attributed") + XCTAssertNil(PixelFiringMock.lastPixel) XCTAssertTrue(fetcherStorage.wasAttributionReportSuccessful) XCTAssertTrue(result) } From 5798fceb5054c24dd16e9a3bfb2a14041ec3842a Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 12 Sep 2024 17:25:49 +0100 Subject: [PATCH 25/31] BSK - Add feature flag for SKAN API (#3356) Task/Issue URL: https://app.asana.com/0/1204167627774280/1208294274484279/f **Description**: Update BSK with feature flag for SKAN No other changes --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a270aad407..d609c580ca 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.1.0; + version = 193.2.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3c522a6bbf..17a7b49fbc 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" : "b55c174793e630d6ec4e0ef471f32a557697cc21", - "version" : "193.1.0" + "revision" : "0021e32b01baca06372e3479d4a33980d7af6618", + "version" : "193.2.0" } }, { From 90b9d3d3106a135a3437d0efa859964294cb6cec Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 12 Sep 2024 13:26:59 -0700 Subject: [PATCH 26/31] Update survey builder OS version (#3348) Task/Issue URL: https://app.asana.com/0/414235014887631/1208279548359774/f Tech Design URL: CC: Description: Updates the client for duckduckgo/BrowserServicesKit#984 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d609c580ca..9ece20c422 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.2.0; + version = 193.2.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17a7b49fbc..a54a4f059b 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" : "0021e32b01baca06372e3479d4a33980d7af6618", - "version" : "193.2.0" + "revision" : "e304d397d61f74a43453748bdc86f933e3fe5425", + "version" : "193.2.1" } }, { From 1b4e29a3d732fc8e6cc2a1d78a45bcecf729badd Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Thu, 12 Sep 2024 16:01:25 -0500 Subject: [PATCH 27/31] Ensure toast closures are called on the main thread (#3347) --- DuckDuckGo/MainViewController.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index ca18ab0e12..edf27a7393 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -1381,14 +1381,18 @@ class MainViewController: UIViewController { private func makeBrokenSitePromptViewHostingController(event: UserBehaviorEvent) -> UIHostingController { let viewModel = BrokenSitePromptViewModel(onDidDismiss: { [weak self] in - self?.hideNotification() - self?.brokenSitePromptLimiter.didDismissToast() - self?.brokenSitePromptViewHostingController = nil + Task { @MainActor in + self?.hideNotification() + self?.brokenSitePromptLimiter.didDismissToast() + self?.brokenSitePromptViewHostingController = nil + } }, onDidSubmit: { [weak self] in - self?.segueToReportBrokenSite(entryPoint: .prompt(event.rawValue)) - self?.hideNotification() - self?.brokenSitePromptLimiter.didOpenReport() - self?.brokenSitePromptViewHostingController = nil + Task { @MainActor in + self?.segueToReportBrokenSite(entryPoint: .prompt(event.rawValue)) + self?.hideNotification() + self?.brokenSitePromptLimiter.didOpenReport() + self?.brokenSitePromptViewHostingController = nil + } }) return UIHostingController(rootView: BrokenSitePromptView(viewModel: viewModel), ignoreSafeArea: true) } From d5d9fbe6bafc31cfb85bc6dc5872e5bb1bd4b749 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 13 Sep 2024 13:14:12 +0500 Subject: [PATCH 28/31] update for macOS: visited links (#3353) Task/Issue URL: https://app.asana.com/0/1175293949586521/1204211850543327/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/992 macOS PR: https://github.com/duckduckgo/macos-browser/pull/3261 --- Core/HistoryManager.swift | 6 +++--- Core/SyncErrorHandler.swift | 6 +++--- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Core/HistoryManager.swift b/Core/HistoryManager.swift index 272bee87dd..0020468071 100644 --- a/Core/HistoryManager.swift +++ b/Core/HistoryManager.swift @@ -88,7 +88,7 @@ public class HistoryManager: HistoryManaging { let baseDomain = tld.eTLDplus1(domain) else { return } await withCheckedContinuation { continuation in - historyCoordinator.burnDomains([baseDomain], tld: tld) { + historyCoordinator.burnDomains([baseDomain], tld: tld) { _ in continuation.resume() } } @@ -137,8 +137,8 @@ class NullHistoryCoordinator: HistoryCoordinating { completion() } - func burnDomains(_ baseDomains: Set, tld: Common.TLD, completion: @escaping () -> Void) { - completion() + func burnDomains(_ baseDomains: Set, tld: Common.TLD, completion: @escaping (Set) -> Void) { + completion([]) } func burnVisits(_ visits: [History.Visit], completion: @escaping () -> Void) { diff --git a/Core/SyncErrorHandler.swift b/Core/SyncErrorHandler.swift index 3c29fb3a99..a3ff07e794 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -39,21 +39,21 @@ public enum AsyncErrorType: String { public class SyncErrorHandler: EventMapping { @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) - private (set) public var isSyncBookmarksPaused: Bool { + private(set) public var isSyncBookmarksPaused: Bool { didSet { isSyncPausedChangedPublisher.send() } } @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) - private (set) public var isSyncCredentialsPaused: Bool { + private(set) public var isSyncCredentialsPaused: Bool { didSet { isSyncPausedChangedPublisher.send() } } @UserDefaultsWrapper(key: .syncIsPaused, defaultValue: false) - private (set) public var isSyncPaused: Bool { + private(set) public var isSyncPaused: Bool { didSet { isSyncPausedChangedPublisher.send() } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9ece20c422..e4d39f99ee 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.2.1; + version = 194.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a54a4f059b..55a375cacb 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" : "e304d397d61f74a43453748bdc86f933e3fe5425", - "version" : "193.2.1" + "revision" : "026acbd36fb80c95e0bfc6a9080e369dd85db66f", + "version" : "194.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" From 1c736e9c4d9befca0a42ed280b4d47c2a5f38800 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 13 Sep 2024 12:37:44 +0200 Subject: [PATCH 29/31] Enroll all internal users in experiment && Update BSK (#3359) Task/Issue URL: https://app.asana.com/0/1204099484721401/1208292211771741/f BSK PR: duckduckgo/BrowserServicesKit#994 Description: We want all internal users enrolled in the experiment Update in BSK to patch Privacy Config, so 'internal' is translated to 'enabled' when privacyConfig is passed to the FE --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/AutofillLoginListViewModel.swift | 2 +- .../DuckPlayer/DuckPlayerLaunchExperiment.swift | 12 ++++++++++-- DuckDuckGo/ImportPasswordsViewModel.swift | 2 +- DuckDuckGo/SpeechRecognizer.swift | 2 +- .../TabViewControllerBrowsingMenuExtension.swift | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e4d39f99ee..d9f651fc28 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 194.0.0; + version = 194.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 55a375cacb..c5522d379b 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" : "026acbd36fb80c95e0bfc6a9080e369dd85db66f", - "version" : "194.0.0" + "revision" : "09b4901eeab71625c4796c0819d0066278b7b6d6", + "version" : "194.1.0" } }, { diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 5c2ff7a615..0bf99e6fe1 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -229,7 +229,7 @@ final class AutofillLoginListViewModel: ObservableObject { authenticator.logOut() } - func authenticate(completion: @escaping(AutofillLoginListAuthenticator.AuthError?) -> Void) { + func authenticate(completion: @escaping (AutofillLoginListAuthenticator.AuthError?) -> Void) { guard !isAuthenticating else { return } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift index b809c7d5b9..19decaf548 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift @@ -91,6 +91,8 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { @UserDefaultsWrapper(key: .duckPlayerPixelExperimentCohort, defaultValue: nil) var experimentCohort: String? + private var isInternalUser: Bool + enum Cohort: String { case control case experiment @@ -100,11 +102,13 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { referrer: DuckPlayerReferrer? = nil, userDefaults: UserDefaults = UserDefaults.standard, pixel: DuckPlayerExperimentPixelFiring.Type = Pixel.self, - dateProvider: DuckPlayerExperimentDateProvider = DefaultDuckPlayerExperimentDateProvider()) { + dateProvider: DuckPlayerExperimentDateProvider = DefaultDuckPlayerExperimentDateProvider(), + isInternalUser: Bool = false) { self.referrer = referrer self.duckPlayerMode = duckPlayerMode self.pixel = pixel self.dateProvider = dateProvider + self.isInternalUser = isInternalUser } private var dates: (day: Int, week: Int)? { @@ -140,7 +144,11 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { func assignUserToCohort() { if !isEnrolled { - let cohort: Cohort = Bool.random() ? .experiment : .control + var cohort: Cohort = Bool.random() ? .experiment : .control + + if isInternalUser { + cohort = .experiment + } experimentCohort = cohort.rawValue enrollmentDate = dateProvider.currentDate fireEnrollmentPixel() diff --git a/DuckDuckGo/ImportPasswordsViewModel.swift b/DuckDuckGo/ImportPasswordsViewModel.swift index 480eb27ef4..46b0e20cc5 100644 --- a/DuckDuckGo/ImportPasswordsViewModel.swift +++ b/DuckDuckGo/ImportPasswordsViewModel.swift @@ -79,7 +79,7 @@ final class ImportPasswordsViewModel { /// Keeping track on whether or not either button was pressed on this screen /// so that a pixel can be fired if the user navigates away without taking any action - private (set) var buttonWasPressed: Bool = false + private(set) var buttonWasPressed: Bool = false func maxButtonWidth() -> CGFloat { let maxWidth = maxWidthFor(title1: ButtonType.getBrowser.title, title2: ButtonType.sync.title) diff --git a/DuckDuckGo/SpeechRecognizer.swift b/DuckDuckGo/SpeechRecognizer.swift index 67e58d9377..119fbfedb8 100644 --- a/DuckDuckGo/SpeechRecognizer.swift +++ b/DuckDuckGo/SpeechRecognizer.swift @@ -101,7 +101,7 @@ final class SpeechRecognizer: NSObject, SpeechRecognizerProtocol { func startRecording(resultHandler: @escaping (_ text: String?, _ error: Error?, _ speechDidFinish: Bool) -> Void, - volumeCallback: @escaping(_ volume: Float) -> Void) { + volumeCallback: @escaping (_ volume: Float) -> Void) { recognitionRequest = SFSpeechAudioBufferRecognitionRequest() audioEngine = AVAudioEngine() diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 0743776c30..12ef1e53ee 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -372,7 +372,7 @@ extension TabViewController { private func shareLinkWithTemporaryDownload(_ temporaryDownload: Download?, originalLink: Link, - completion: @escaping(Link) -> Void) { + completion: @escaping (Link) -> Void) { guard let download = temporaryDownload else { completion(originalLink) return From 7be8bf67f10241a206556f612ca399b846672755 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Fri, 13 Sep 2024 16:11:24 +0100 Subject: [PATCH 30/31] SKAD4 crash fix (#3361) Task/Issue URL: https://app.asana.com/0/414709148257752/1208303110742725/f Fix for a logging string interpolation in SKAD4 attribution. I think this could be an internal AdAttributionKit error because every thrown error should be not nil and responding to `localisedDescription`, but here looks like something is going wrong when we try to log `Logger.general.error("Attribution: SKAN 4 postback failed \(error.localizedDescription, privacy: .public)")` and accessing `error.localizedDescription ` This attempt tries not to assume anything about the error and just logs a string representation. --- DuckDuckGo/AppDelegate+Attribution.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/AppDelegate+Attribution.swift b/DuckDuckGo/AppDelegate+Attribution.swift index e82dd002e3..bfef272bf4 100644 --- a/DuckDuckGo/AppDelegate+Attribution.swift +++ b/DuckDuckGo/AppDelegate+Attribution.swift @@ -43,7 +43,7 @@ extension AppDelegate { try await AdAttributionKit.Postback.updateConversionValue(conversionValue, coarseConversionValue: .high, lockPostback: true) Logger.general.debug("Attribution: AdAttributionKit postback succeeded") } catch { - Logger.general.error("Attribution: AdAttributionKit postback failed \(error.localizedDescription, privacy: .public)") + Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") } } @@ -52,8 +52,8 @@ extension AppDelegate { do { try await SKAdNetwork.updatePostbackConversionValue(conversionValue, coarseValue: .high, lockWindow: true) Logger.general.debug("Attribution: SKAN 4 postback succeeded") - } catch { - Logger.general.error("Attribution: SKAN 4 postback failed \(error.localizedDescription, privacy: .public)") + } catch let error { + Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") } } From 283411894ef58ed258e0ad80f09228927d7f8eb2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 13 Sep 2024 20:55:02 +0100 Subject: [PATCH 31/31] Add Marketplace Postback handling (#3357) Task/Issue URL: https://app.asana.com/0/72649045549333/1208126219488944/f Tech Design URL: https://app.asana.com/0/72649045549333/1208274586553401/f **Description**: Add Marketplace Postback handling --- Core/MarketplaceAdPostback.swift | 93 +++++++++++++++++ Core/MarketplaceAdPostbackManager.swift | 74 ++++++++++++++ Core/MarketplaceAdPostbackStorage.swift | 62 ++++++++++++ Core/MarketplaceAdPostbackUpdater.swift | 81 +++++++++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 40 +++++++- DuckDuckGo/AppDelegate+Attribution.swift | 60 ----------- DuckDuckGo/AppDelegate.swift | 17 +++- DuckDuckGo/Info.plist | 2 + .../MarketplaceAdPostbackManagerTests.swift | 99 +++++++++++++++++++ 9 files changed, 460 insertions(+), 68 deletions(-) create mode 100644 Core/MarketplaceAdPostback.swift create mode 100644 Core/MarketplaceAdPostbackManager.swift create mode 100644 Core/MarketplaceAdPostbackStorage.swift create mode 100644 Core/MarketplaceAdPostbackUpdater.swift delete mode 100644 DuckDuckGo/AppDelegate+Attribution.swift create mode 100644 DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift diff --git a/Core/MarketplaceAdPostback.swift b/Core/MarketplaceAdPostback.swift new file mode 100644 index 0000000000..d8d9a5ff5a --- /dev/null +++ b/Core/MarketplaceAdPostback.swift @@ -0,0 +1,93 @@ +// +// MarketplaceAdPostback.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 StoreKit +import AdAttributionKit + +enum MarketplaceAdPostback { + case installNewUser + case installReturningUser + + /// An enumeration representing coarse conversion values for both SKAdNetwork and AdAttributionKit. + /// + /// This enum provides a unified interface to handle coarse conversion values, which are used in both SKAdNetwork and AdAttributionKit. + /// Despite having the same value names (`low`, `medium`, `high`), the types for these values differ between the two frameworks. + /// This wrapper simplifies the usage by providing a common interface. + /// + /// - Cases: + /// - `low`: Represents a low conversion value. + /// - `medium`: Represents a medium conversion value. + /// - `high`: Represents a high conversion value. + /// + /// - Properties: + /// - `coarseConversionValue`: Available on iOS 17.4 and later, this property returns the corresponding `CoarseConversionValue` from AdAttributionKit. + /// - `skAdCoarseConversionValue`: Available on iOS 16.1 and later, this property returns the corresponding `SKAdNetwork.CoarseConversionValue`. + /// + enum CoarseConversion { + case low + case medium + case high + + /// Returns the corresponding `CoarseConversionValue` from AdAttributionKit. + @available(iOS 17.4, *) + var coarseConversionValue: CoarseConversionValue { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + + /// Returns the corresponding `SKAdNetwork.CoarseConversionValue`. + @available(iOS 16.1, *) + var skAdCoarseConversionValue: SKAdNetwork.CoarseConversionValue { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + } + + // https://app.asana.com/0/0/1208126219488943/f + var fineValue: Int { + switch self { + case .installNewUser: return 0 + case .installReturningUser: return 1 + } + } + + var coarseValue: CoarseConversion { + switch self { + case .installNewUser: return .high + case .installReturningUser: return .low + } + } + + @available(iOS 17.4, *) + var adAttributionKitCoarseValue: CoarseConversionValue { + return coarseValue.coarseConversionValue + } + + @available(iOS 16.1, *) + var SKAdCoarseValue: SKAdNetwork.CoarseConversionValue { + return coarseValue.skAdCoarseConversionValue + } +} diff --git a/Core/MarketplaceAdPostbackManager.swift b/Core/MarketplaceAdPostbackManager.swift new file mode 100644 index 0000000000..32cfbdea7f --- /dev/null +++ b/Core/MarketplaceAdPostbackManager.swift @@ -0,0 +1,74 @@ +// +// MarketplaceAdPostbackManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public protocol MarketplaceAdPostbackManaging { + + /// Updates the install postback based on the return user measurement + /// + /// This method determines whether the user is a returning user or a new user and sends the appropriate postback value: + /// - If the user is returning, it sends the `appLaunchReturningUser` postback value. + /// - If the user is new, it sends the `appLaunchNewUser` postback value. + /// + /// > For the time being, we're also sending `lockPostback` to `true`. + /// > More information can be found [here](https://app.asana.com/0/0/1208126219488943/1208289369964239/f). + func sendAppLaunchPostback() + + /// Updates the stored value for the returning user state. + /// + /// This method updates the storage with the current state of the user (returning or new). + /// Since `ReturnUserMeasurement` will always return `isReturningUser` as `false` after the first run, + /// `MarketplaceAdPostbackManaging` maintains its own storage of the user's state across app launches. + func updateReturningUserValue() +} + +public struct MarketplaceAdPostbackManager: MarketplaceAdPostbackManaging { + private let storage: MarketplaceAdPostbackStorage + private let updater: MarketplaceAdPostbackUpdating + private let returningUserMeasurement: ReturnUserMeasurement + + internal init(storage: MarketplaceAdPostbackStorage = UserDefaultsMarketplaceAdPostbackStorage(), + updater: MarketplaceAdPostbackUpdating = MarketplaceAdPostbackUpdater(), + returningUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement()) { + self.storage = storage + self.updater = updater + self.returningUserMeasurement = returningUserMeasurement + } + + public init() { + self.storage = UserDefaultsMarketplaceAdPostbackStorage() + self.updater = MarketplaceAdPostbackUpdater() + self.returningUserMeasurement = KeychainReturnUserMeasurement() + } + + public func sendAppLaunchPostback() { + guard let isReturningUser = storage.isReturningUser else { return } + + if isReturningUser { + updater.updatePostback(.installReturningUser, lockPostback: true) + } else { + updater.updatePostback(.installNewUser, lockPostback: true) + } + } + + public func updateReturningUserValue() { + storage.updateReturningUserValue(returningUserMeasurement.isReturningUser) + } +} diff --git a/Core/MarketplaceAdPostbackStorage.swift b/Core/MarketplaceAdPostbackStorage.swift new file mode 100644 index 0000000000..66734f1818 --- /dev/null +++ b/Core/MarketplaceAdPostbackStorage.swift @@ -0,0 +1,62 @@ +// +// MarketplaceAdPostbackStorage.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 + +/// A protocol defining the storage for marketplace ad postback data. +protocol MarketplaceAdPostbackStorage { + + /// A Boolean value indicating whether the user is a returning user. + /// + /// If the value is `nil`, it means the storage was never set. + var isReturningUser: Bool? { get } + + /// Updates the stored value indicating whether the user is a returning user. + /// + /// - Parameter value: A Boolean value indicating whether the user is a returning user. + func updateReturningUserValue(_ value: Bool) +} + +/// A concrete implementation of `MarketplaceAdPostbackStorage` that uses `UserDefaults` for storage. +struct UserDefaultsMarketplaceAdPostbackStorage: MarketplaceAdPostbackStorage { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + var isReturningUser: Bool? { + userDefaults.isReturningUser + } + + func updateReturningUserValue(_ value: Bool) { + userDefaults.isReturningUser = value + } +} + +private extension UserDefaults { + enum Keys { + static let isReturningUser = "marketplaceAdPostback.isReturningUser" + } + + var isReturningUser: Bool? { + get { object(forKey: Keys.isReturningUser) as? Bool } + set { set(newValue, forKey: Keys.isReturningUser) } + } +} diff --git a/Core/MarketplaceAdPostbackUpdater.swift b/Core/MarketplaceAdPostbackUpdater.swift new file mode 100644 index 0000000000..0b8d3a819b --- /dev/null +++ b/Core/MarketplaceAdPostbackUpdater.swift @@ -0,0 +1,81 @@ +// +// MarketplaceAdPostbackUpdater.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 AdAttributionKit +import os.log +import StoreKit + +/// Updates anonymous attribution values. +/// +/// DuckDuckGo uses the SKAdNetwork framework to monitor anonymous install attribution data. +/// No personally identifiable data is involved. +/// DuckDuckGo does not use the App Tracking Transparency framework at any point. +/// See https://developer.apple.com/documentation/storekit/skadnetwork/ for details. +/// + +protocol MarketplaceAdPostbackUpdating { + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) +} + +struct MarketplaceAdPostbackUpdater: MarketplaceAdPostbackUpdating { + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { +#if targetEnvironment(simulator) + Logger.general.debug("Attribution: Postback doesn't work on simulators, returning early...") +#else + if #available(iOS 17.4, *) { + // https://developer.apple.com/documentation/adattributionkit/adattributionkit-skadnetwork-interoperability + Task { + await updateAdAttributionKitPostback(postback, lockPostback: lockPostback) + } + updateSKANPostback(postback, lockPostback: lockPostback) + } else if #available(iOS 16.1, *) { + updateSKANPostback(postback, lockPostback: lockPostback) + } +#endif + } + + @available(iOS 17.4, *) + private func updateAdAttributionKitPostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) async { + do { + try await AdAttributionKit.Postback.updateConversionValue(postback.fineValue, + coarseConversionValue: postback.adAttributionKitCoarseValue, + lockPostback: lockPostback) + Logger.general.debug("Attribution: AdAttributionKit postback succeeded") + } catch { + Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") + } + } + + @available(iOS 16.1, *) + private func updateSKANPostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { + /// Switched to using the completion handler API instead of async due to an encountered error. + /// Error report: + /// https://errors.duckduckgo.com/organizations/ddg/issues/104096/events/ab29c80e711f11efbf32499bdc26619c/ + + SKAdNetwork.updatePostbackConversionValue(postback.fineValue, + coarseValue: postback.SKAdCoarseValue) { error in + if let error = error { + Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") + } else { + Logger.general.debug("Attribution: SKAN 4 postback succeeded") + } + } + } +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d9f651fc28..d751612bbe 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -154,10 +154,12 @@ 3158461A281B08F5004ADB8B /* AutofillLoginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31584619281B08F5004ADB8B /* AutofillLoginListViewModel.swift */; }; 3161D13227AC161B00285CF6 /* DownloadMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3161D13127AC161B00285CF6 /* DownloadMetadata.swift */; }; 31669B9A28020A460071CC18 /* SaveLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31669B9928020A460071CC18 /* SaveLoginViewModel.swift */; }; + 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */; }; 316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */; }; 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */; }; 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; }; 317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; }; + 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; }; 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; }; 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; }; 319A371028299A850079FBCE /* PasswordHider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319A370F28299A850079FBCE /* PasswordHider.swift */; }; @@ -166,6 +168,9 @@ 31A42564285A09E800049386 /* FaviconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42563285A09E800049386 /* FaviconView.swift */; }; 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42565285A0A6300049386 /* FaviconViewModel.swift */; }; 31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */; }; + 31B2F10F2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */; }; + 31B2F1112C92FEE000CD30E3 /* MarketplaceAdPostback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */; }; + 31B2F1132C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */; }; 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F11E287846320040427A /* NoMicPermissionAlert.swift */; }; 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B524562715BB23002225AB /* WebJSAlert.swift */; }; 31BC5F412C2B0B540004DF37 /* DuckPlayer.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31BC5F402C2B0B540004DF37 /* DuckPlayer.xcassets */; }; @@ -1065,7 +1070,6 @@ F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */; }; F15531942BF215F60029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F15531932BF215F60029ED04 /* Subscription */; }; F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */; }; - F1564F032B7B915F00D454A6 /* AppDelegate+Attribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */; }; F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */; }; F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */; }; F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C141E57336D00DEDCAF /* TabManager.swift */; }; @@ -1433,11 +1437,13 @@ 31584619281B08F5004ADB8B /* AutofillLoginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListViewModel.swift; sourceTree = ""; }; 3161D13127AC161B00285CF6 /* DownloadMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMetadata.swift; sourceTree = ""; }; 31669B9928020A460071CC18 /* SaveLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveLoginViewModel.swift; sourceTree = ""; }; + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManagerTests.swift; sourceTree = ""; }; 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToDownloadsAlert.swift; sourceTree = ""; }; 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadActionMessageViewHelper.swift; sourceTree = ""; }; 3170048127A9504F00C03F35 /* DownloadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMocks.swift; sourceTree = ""; }; 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = ""; }; 31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = ""; }; + 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = ""; }; 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = ""; }; 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = ""; }; 319A370F28299A850079FBCE /* PasswordHider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordHider.swift; sourceTree = ""; }; @@ -1446,6 +1452,9 @@ 31A42563285A09E800049386 /* FaviconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconView.swift; sourceTree = ""; }; 31A42565285A0A6300049386 /* FaviconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconViewModel.swift; sourceTree = ""; }; 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; + 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManager.swift; sourceTree = ""; }; + 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostback.swift; sourceTree = ""; }; + 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackUpdater.swift; sourceTree = ""; }; 31B2F11E287846320040427A /* NoMicPermissionAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoMicPermissionAlert.swift; sourceTree = ""; }; 31B524562715BB23002225AB /* WebJSAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebJSAlert.swift; sourceTree = ""; }; 31BC5F402C2B0B540004DF37 /* DuckPlayer.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DuckPlayer.xcassets; sourceTree = ""; }; @@ -2891,7 +2900,6 @@ F143C32C1E4A9A4800CFDE3A /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIViewControllerExtension.swift; path = ../Core/UIViewControllerExtension.swift; sourceTree = ""; }; F143C3451E4AA32D00CFDE3A /* SearchBarExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchBarExtension.swift; path = ../Core/SearchBarExtension.swift; sourceTree = ""; }; F14E491E1E391CE900DC037C /* URLExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtensionTests.swift; sourceTree = ""; }; - F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Attribution.swift"; sourceTree = ""; }; F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteViewController.swift; sourceTree = ""; }; F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherViewController.swift; sourceTree = ""; }; @@ -3514,6 +3522,14 @@ name = LoginDetails; sourceTree = ""; }; + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 316931DA27BD24B60095F5ED /* Alerts */ = { isa = PBXGroup; children = ( @@ -3564,6 +3580,17 @@ name = Table; sourceTree = ""; }; + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */, + 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */, + 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */, + 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 31C138A127A334F600FFD4B2 /* Downloads */ = { isa = PBXGroup; children = ( @@ -5774,6 +5801,7 @@ F143C2E51E4A4CD400CFDE3A /* Core */ = { isa = PBXGroup; children = ( + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */, F1CE42A71ECA0A520074A8DF /* Bookmarks */, 837774491F8E1ECE00E17A29 /* ContentBlocker */, F143C2E61E4A4CD400CFDE3A /* Core.h */, @@ -6097,7 +6125,6 @@ CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */, - F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */, 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, 85BA58541F34F49E00C6E8CA /* AppUserDefaults.swift */, @@ -6242,6 +6269,7 @@ F1E092B31E92A6B900732CCC /* Core */ = { isa = PBXGroup; children = ( + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */, 858479CA2B8795BF00D156C1 /* History */, EA7EFE662677F5BD0075464E /* PrivacyReferenceTests */, 83EDCC3E1F86B363005CDFCD /* API */, @@ -7330,7 +7358,6 @@ 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, 98D98A7425ED88D100D8E3DF /* BrowsingMenuEntryViewCell.swift in Sources */, - F1564F032B7B915F00D454A6 /* AppDelegate+Attribution.swift in Sources */, 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */, 4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */, CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */, @@ -7920,6 +7947,7 @@ 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */, 314A3EFC293905EC00D3D4C8 /* BrokenSiteReportingTests.swift in Sources */, 851B1283221FE65E004781BC /* ImproveOnboardingExperiment1Tests.swift in Sources */, + 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */, F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */, BBFF18B12C76448100C48D7D /* QuerySubmittedTests.swift in Sources */, 854A8D812C7F4452001D62E5 /* AtbTests.swift in Sources */, @@ -8100,6 +8128,7 @@ 851624C22B95F8BD002D5CD7 /* HistoryCapture.swift in Sources */, 85CA53AC24BBD39300A6288C /* FaviconRequestModifier.swift in Sources */, CB258D1D29A52AF900DEBA24 /* EtagStorage.swift in Sources */, + 31B2F10F2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift in Sources */, C1B7B52D2894469D0098FD6A /* DefaultVariantManager.swift in Sources */, 9833913727AC400800DAF119 /* AppTrackerDataSetProvider.swift in Sources */, 83004E802193BB8200DA013C /* WKNavigationExtension.swift in Sources */, @@ -8134,6 +8163,7 @@ 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */, 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */, EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */, + 31B2F1112C92FEE000CD30E3 /* MarketplaceAdPostback.swift in Sources */, CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */, BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */, 98629D312C21765A001E6031 /* BookmarksStateValidation.swift in Sources */, @@ -8158,6 +8188,7 @@ 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */, 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */, B652DF10287C2C1600C12A9C /* ContentBlocking.swift in Sources */, + 31B2F1132C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift in Sources */, 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, 85BA79911F6FF75000F59015 /* ContentBlockerStoreConstants.swift in Sources */, 85E242172AB1B54D000F3E28 /* ReturnUserMeasurement.swift in Sources */, @@ -8184,6 +8215,7 @@ 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */, 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */, 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */, + 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */, EE50053029C3BA0800AE0773 /* InternalUserStore.swift in Sources */, F1D477CB1F2149C40031ED49 /* Type.swift in Sources */, 983C52E42C2C050B007B5747 /* BookmarksStateRepair.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate+Attribution.swift b/DuckDuckGo/AppDelegate+Attribution.swift deleted file mode 100644 index bfef272bf4..0000000000 --- a/DuckDuckGo/AppDelegate+Attribution.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AppDelegate+Attribution.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common -import StoreKit -import AdAttributionKit -import os.log - -extension AppDelegate { - - func updateAttribution(conversionValue: Int) { - Task { - if #available(iOS 17.4, *) { - // https://developer.apple.com/documentation/adattributionkit/adattributionkit-skadnetwork-interoperability - await updateAdAttributionKitPostback(conversionValue: conversionValue) - await updateSKANPostback(conversionValue: conversionValue) - } else if #available(iOS 16.1, *) { - await updateSKANPostback(conversionValue: conversionValue) - } - } - } - - @available(iOS 17.4, *) - private func updateAdAttributionKitPostback(conversionValue: Int) async { - do { - try await AdAttributionKit.Postback.updateConversionValue(conversionValue, coarseConversionValue: .high, lockPostback: true) - Logger.general.debug("Attribution: AdAttributionKit postback succeeded") - } catch { - Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") - } - } - - @available(iOS 16.1, *) - private func updateSKANPostback(conversionValue: Int) async { - do { - try await SKAdNetwork.updatePostbackConversionValue(conversionValue, coarseValue: .high, lockWindow: true) - Logger.general.debug("Attribution: SKAN 4 postback succeeded") - } catch let error { - Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") - } - } - -} diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 9bd1828a58..50b89c4358 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -104,6 +104,7 @@ import os.log private let launchOptionsHandler = LaunchOptionsHandler() private let onboardingPixelReporter = OnboardingPixelReporter() + private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() override init() { super.init() @@ -117,9 +118,6 @@ import os.log // swiftlint:disable:next function_body_length func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Attribution support - updateAttribution(conversionValue: 1) - #if targetEnvironment(simulator) if ProcessInfo.processInfo.environment["UITESTING"] == "true" { // Disable hardware keyboards. @@ -523,6 +521,7 @@ import os.log } AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { ContentBlocking.shared.contentBlockingManager.scheduleCompilation() } @@ -757,6 +756,14 @@ import os.log // MARK: private + private func sendAppLaunchPostback() { + // Attribution support + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { + marketplaceAdPostbackManager.sendAppLaunchPostback() + } + } + private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { let historyMessageManager = HistoryMessageManager() @@ -772,6 +779,9 @@ import os.log // New users don't see the message historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() } } @@ -968,7 +978,6 @@ import os.log UIApplication.shared.shortcutItems = nil } } - } extension AppDelegate: BlankSnapshotViewRecoveringDelegate { diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index a19b12c025..6d3941b0ca 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -240,6 +240,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAdvertisingAttributionReportEndpoint + https://duckduckgo.com UIViewControllerBasedStatusBarAppearance diff --git a/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift b/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift new file mode 100644 index 0000000000..332aa04d7b --- /dev/null +++ b/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift @@ -0,0 +1,99 @@ +// +// MarketplaceAdPostbackManagerTests.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 Core +import Foundation + +class MarketplaceAdPostbackManagerTests: XCTestCase { + func testSendAppLaunchPostback_NewUser() { + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: false) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: false) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installNewUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .high) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } + + func testSendAppLaunchPostback_ReturningUser() { + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: true) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: true) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installReturningUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .low) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } + + func testSendAppLaunchPostback_AfterMeasurementChangesState() { + /// Sets return user to true to mock the situation where the user is opening the app again + /// If the storage is set to false, it should still be set as new user + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: true) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: false) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installNewUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .high) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } +} + +private final class MockReturnUserMeasurement: ReturnUserMeasurement { + func installCompletedWithATB(_ atb: Core.Atb) { } + + func updateStoredATB(_ atb: Core.Atb) { } + + var isReturningUser: Bool + + init(isReturningUser: Bool) { + self.isReturningUser = isReturningUser + } +} + +private final class MockMarketplaceAdPostbackUpdater: MarketplaceAdPostbackUpdating { + var postbackSent: MarketplaceAdPostback? + var lockPostbackSent: Bool? + + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { + postbackSent = postback + lockPostbackSent = lockPostback + } +} + +private final class MockMarketplaceAdPostbackStorage: MarketplaceAdPostbackStorage { + var isReturningUser: Bool? + + init(isReturningUser: Bool?) { + self.isReturningUser = isReturningUser + } + + func updateReturningUserValue(_ value: Bool) { + isReturningUser = value + } +}