Skip to content

Commit

Permalink
Onboarding Highlights Ship Review (#3380)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1206329551987282/1208291253556033/f

**Description**:
This PR includes changes and fixes that have been addressed during the Ship Review.
  • Loading branch information
alessandroboron authored Sep 26, 2024
1 parent b95013e commit 3184192
Show file tree
Hide file tree
Showing 61 changed files with 2,386 additions and 219 deletions.
10 changes: 10 additions & 0 deletions .maestro/shared/onboarding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ appId: com.duckduckgo.mobile.ios
# - assertVisible: "Make DuckDuckGo your default browser."
- tapOn:
text: "Skip"

- runFlow:
when:
visible: "Which color looks best on me?"
commands:
- assertVisible: "Next"
- tapOn: "Next"
- assertVisible: "Where should I put your address bar?"
- assertVisible: "Next"
- tapOn: "Next"
66 changes: 66 additions & 0 deletions Core/Debouncer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// Debouncer.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 class that provides a debouncing mechanism.
public final class Debouncer {
private let runLoop: RunLoop
private let mode: RunLoop.Mode
private var timer: Timer?

/// Initializes a new instance of `Debouncer`.
///
/// - Parameters:
/// - runLoop: The `RunLoop` on which the debounced actions will be scheduled. Defaults to the current run loop.
///
/// - mode: The `RunLoop.Mode` in which the debounced actions will be scheduled. Defaults to `.default`.
///
/// Use `RunLoop.main` for UI-related actions to ensure they run on the main thread.
public init(runLoop: RunLoop = .current, mode: RunLoop.Mode = .default) {
self.runLoop = runLoop
self.mode = mode
}

/// Debounces the provided block of code, executing it after a specified time interval elapses.
/// - Parameters:
/// - dueTime: The time interval (in seconds) to wait before executing the block.
/// - block: The closure to execute after the due time has passed.
///
/// If `dueTime` is less than or equal to zero, the block is executed immediately.
public func debounce(for dueTime: TimeInterval, block: @escaping () -> Void) {
timer?.invalidate()

guard dueTime > 0 else { return block() }

let timer = Timer(timeInterval: dueTime, repeats: false, block: { timer in
guard timer.isValid else { return }
block()
})

runLoop.add(timer, forMode: mode)
self.timer = timer
}

/// Cancels any pending execution of the debounced block.
public func cancel() {
timer?.invalidate()
timer = nil
}
}
7 changes: 5 additions & 2 deletions Core/DefaultVariantManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ extension FeatureName {
// public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature")

public static let newOnboardingIntro = FeatureName(rawValue: "newOnboardingIntro")
public static let newOnboardingIntroHighlights = FeatureName(rawValue: "newOnboardingIntroHighlights")
public static let contextualDaxDialogs = FeatureName(rawValue: "contextualDaxDialogs")
}

public struct VariantIOS: Variant {
Expand Down Expand Up @@ -56,8 +58,9 @@ public struct VariantIOS: Variant {
VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []),
VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []),

VariantIOS(name: "ma", weight: 1, isIncluded: When.always, features: []),
VariantIOS(name: "mb", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]),
VariantIOS(name: "ms", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]),
VariantIOS(name: "mu", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro, .contextualDaxDialogs]),
VariantIOS(name: "mx", weight: 1, isIncluded: When.always, features: [.newOnboardingIntroHighlights, .contextualDaxDialogs]),

returningUser
]
Expand Down
28 changes: 20 additions & 8 deletions Core/NSAttributedStringExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ extension NSAttributedString {
}
}

// MARK: - AttributedString Helper Extensions

public extension String {

var attributed: NSAttributedString {
NSAttributedString(string: self)
}

var nsRange: NSRange {
NSRange(startIndex..., in: self)
}

func range(of string: String) -> NSRange {
(self as NSString).range(of: string)
}

}

// MARK: Helper Operators

/// Concatenates two `NSAttributedString` instances, returning a new `NSAttributedString`.
///
/// - Parameters:
Expand Down Expand Up @@ -115,11 +135,3 @@ public func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString {
public func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString {
NSAttributedString(string: lhs) + rhs
}

private extension String {

var nsRange: NSRange {
NSRange(startIndex..., in: self)
}

}
16 changes: 16 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,9 @@
98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; };
98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; };
98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; };
9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; };
9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */; };
9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */; };
9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; };
9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; };
9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; };
Expand All @@ -709,6 +712,7 @@
9F5E5AAC2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AAB2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift */; };
9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AAF2C3E4C6000165F54 /* ContextualOnboardingPresenter.swift */; };
9F5E5AB22C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AB12C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift */; };
9F678B892C9BAA4800CA0E19 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F678B882C9BAA4800CA0E19 /* Debouncer.swift */; };
9F6933192C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6933182C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift */; };
9F69331B2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */; };
9F69331D2C5A191400CD6A5D /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */; };
Expand Down Expand Up @@ -2503,6 +2507,9 @@
98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = "<group>"; };
98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = "<group>"; };
9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = "<group>"; };
9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultVariantManager+Onboarding.swift"; sourceTree = "<group>"; };
9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVariantManagerOnboardingTests.swift; sourceTree = "<group>"; };
9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = "<group>"; };
9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = "<group>"; };
9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = "<group>"; };
Expand All @@ -2517,6 +2524,7 @@
9F5E5AAB2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualDaxDialogsFactory.swift; sourceTree = "<group>"; };
9F5E5AAF2C3E4C6000165F54 /* ContextualOnboardingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenter.swift; sourceTree = "<group>"; };
9F5E5AB12C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterTests.swift; sourceTree = "<group>"; };
9F678B882C9BAA4800CA0E19 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
9F6933182C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterMock.swift; sourceTree = "<group>"; };
9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDaxFavouritesTests.swift; sourceTree = "<group>"; };
9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTutorialSettings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4773,6 +4781,7 @@
9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */,
9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */,
9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */,
9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */,
);
name = Onboarding;
sourceTree = "<group>";
Expand Down Expand Up @@ -4898,6 +4907,7 @@
9F23B7FF2C2BABE000950875 /* OnboardingIntro */,
9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */,
9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */,
9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */,
);
path = OnboardingExperiment;
sourceTree = "<group>";
Expand Down Expand Up @@ -5901,6 +5911,7 @@
56D855692BEA9169009F9698 /* CurrentDateProviding.swift */,
9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */,
9FEA22312C3270BD006B03BF /* TimerInterface.swift */,
9F678B882C9BAA4800CA0E19 /* Debouncer.swift */,
);
name = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -6047,6 +6058,7 @@
8341D804212D5DFB000514C2 /* HashExtensionTest.swift */,
1CB7B82223CEA28300AA24EA /* DateExtensionTests.swift */,
4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */,
9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */,
);
name = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -7428,6 +7440,7 @@
6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */,
9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */,
6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */,
9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */,
1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */,
C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */,
8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */,
Expand Down Expand Up @@ -7892,6 +7905,7 @@
files = (
8528AE84212FF9A100D0BD74 /* AppRatingPromptStorageTests.swift in Sources */,
569437312BE3F64400C0881B /* SyncErrorHandlerSyncPausedAlertsTests.swift in Sources */,
9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */,
1CB7B82323CEA28300AA24EA /* DateExtensionTests.swift in Sources */,
31C138A427A3352600FFD4B2 /* DownloadTests.swift in Sources */,
853A717820F645FB00FE60BC /* PixelTests.swift in Sources */,
Expand Down Expand Up @@ -7947,6 +7961,7 @@
5694372B2BE3F2D900C0881B /* SyncErrorHandlerTests.swift in Sources */,
987130C7294AAB9F00AB05E0 /* MenuBookmarksViewModelTests.swift in Sources */,
858650D32469BFAD00C36F8A /* DaxDialogTests.swift in Sources */,
9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */,
31C138B227A4097800FFD4B2 /* DownloadTestsHelper.swift in Sources */,
1E1D8B5D2994FFE100C96994 /* AutoconsentMessageProtocolTests.swift in Sources */,
85C11E532090B23A00BFFEB4 /* UserDefaultsHomeRowReminderStorageTests.swift in Sources */,
Expand Down Expand Up @@ -8285,6 +8300,7 @@
9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */,
B652DF0D287C2A6300C12A9C /* PrivacyFeatures.swift in Sources */,
F10E522D1E946F8800CE1253 /* NSAttributedStringExtension.swift in Sources */,
9F678B892C9BAA4800CA0E19 /* Debouncer.swift in Sources */,
9887DC252354D2AA005C85F5 /* Database.swift in Sources */,
F143C3171E4A99D200CFDE3A /* AppURLs.swift in Sources */,
C1963863283794A000298D4D /* BookmarksCachingSearch.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "cc3629fa16880e410e588a27a6b2426dcc140009",
"version" : "198.0.1"
"revision" : "4db50292abf1180d66da55cf83f75d37395df1f9",
"version" : "198.1.0"
}
},
{
"identity" : "content-scope-scripts",
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/content-scope-scripts",
"state" : {
"revision" : "4f0d109f13beec7e8beaf0bd5c0e2c1d528677f8",
"version" : "6.16.0"
"revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1",
"version" : "6.17.0"
}
},
{
Expand Down
35 changes: 29 additions & 6 deletions DuckDuckGo/DaxDialogs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ protocol ContextualOnboardingLogic {

func setSearchMessageSeen()
func setFireEducationMessageSeen()
func clearedBrowserData()
func setFinalOnboardingDialogSeen()
func setPrivacyButtonPulseSeen()
func setDaxDialogDismiss()

func canEnableAddFavoriteFlow() -> Bool // Temporary during Contextual Onboarding Experiment
func enableAddFavoriteFlow()
Expand Down Expand Up @@ -227,7 +229,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
}

private var isNewOnboarding: Bool {
variantManager.isSupported(feature: .newOnboardingIntro)
variantManager.isContextualDaxDialogsEnabled
}

private var firstBrowsingMessageSeen: Bool {
Expand Down Expand Up @@ -277,6 +279,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
var isEnabled: Bool {
// skip dax dialogs in integration tests
guard ProcessInfo.processInfo.environment["DAXDIALOGS"] != "false" else { return false }
guard variantManager.shouldShowDaxDialogs else { return false }
return !settings.isDismissed
}

Expand Down Expand Up @@ -317,8 +320,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
func dismiss() {
settings.isDismissed = true
// Reset last shown dialog as we don't have to show it anymore.
removeLastShownDaxDialog()
removeLastVisitedOnboardingWebsite()
clearOnboardingBrowsingData()
}

func primeForUse() {
Expand Down Expand Up @@ -448,11 +450,21 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
saveLastShownDaxDialog(specType: .fire)
}

func clearedBrowserData() {
guard isNewOnboarding else { return }
setDaxDialogDismiss()
}

func setPrivacyButtonPulseSeen() {
guard isNewOnboarding else { return }
settings.privacyButtonPulseShown = true
}

func setDaxDialogDismiss() {
guard isNewOnboarding else { return }
clearOnboardingBrowsingData()
}

func setFinalOnboardingDialogSeen() {
guard isNewOnboarding else { return }
settings.browsingFinalDialogShown = true
Expand Down Expand Up @@ -543,8 +555,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
saveLastShownDaxDialog(specType: spec.type)
saveLastVisitedOnboardingWebsite(url: privacyInfo.url)
} else {
removeLastVisitedOnboardingWebsite()
removeLastShownDaxDialog()
clearOnboardingBrowsingData()
}

return spec
Expand All @@ -561,6 +572,9 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
}

func nextHomeScreenMessageNew() -> HomeScreenSpec? {
// Reset the last browsing information when opening a new tab so loading the previous website won't show again the Dax dialog
clearedBrowserData()

guard let homeScreenSpec = peekNextHomeScreenMessageExperiment() else {
currentHomeSpec = nil
return nil
Expand Down Expand Up @@ -592,10 +606,14 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
if nextHomeScreenMessageOverride != nil {
return nextHomeScreenMessageOverride
}

guard isEnabled else { return nil }

// If the user has already seen the end of journey dialog we don't want to show any other NTP Dax dialog.
guard !finalDaxDialogSeen else { return nil }

// Check final first as if we skip anonymous searches we don't want to show this.
if settings.fireMessageExperimentShown && !finalDaxDialogSeen {
if settings.fireMessageExperimentShown {
return .final
}

Expand Down Expand Up @@ -712,6 +730,11 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {

return url1.isSameDuckDuckGoSearchURL(other: url2)
}

private func clearOnboardingBrowsingData() {
removeLastShownDaxDialog()
removeLastVisitedOnboardingWebsite()
}
}

extension URL {
Expand Down
Loading

0 comments on commit 3184192

Please sign in to comment.