Skip to content

Commit 9a9cd02

Browse files
authored
Updated settings (duckduckgo#2603)
Task/Issue URL: https://app.asana.com/0/0/1206668965195606/f Description: New Settings layout with Privacy Protections, Main Settings, Next Steps and others. It also includes the implementation of pixel experiment for monitoring the usage of old and new Settings to make sure we don't harm the experience. ℹ️ The experiment will be activated in a follow-up PR after running experiments on iOS are over.
1 parent 2b0a8dc commit 9a9cd02

File tree

219 files changed

+16915
-2049
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

219 files changed

+16915
-2049
lines changed

Core/AppURLs.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import BrowserServicesKit
2121
import Foundation
2222

23+
// swiftlint:disable line_length
24+
2325
public extension URL {
2426

2527
private static let base: String = ProcessInfo.processInfo.environment["BASE_URL", default: "https://duckduckgo.com"]
@@ -31,7 +33,12 @@ public extension URL {
3133
static let emailProtection = URL(string: "\(base)/email")!
3234
static let emailProtectionSignUp = URL(string: "\(base)/email/start-incontext")!
3335
static let emailProtectionQuickLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/email"))!
36+
static let emailProtectionAccountLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/email/settings/account"))!
37+
static let emailProtectionSupportLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/email/settings/support"))!
38+
static let emailProtectionHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/email-protection/what-is-duckduckgo-email-protection/"))!
3439
static let aboutLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/about"))!
40+
static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps"))!
41+
static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))!
3542

3643
static let surrogates = URL(string: "\(staticBase)/surrogates.txt")!
3744

@@ -262,3 +269,5 @@ public final class StatisticsDependentURLFactory {
262269
}
263270

264271
}
272+
273+
// swiftlint:enable line_length

Core/Pixel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public struct PixelParameters {
127127
public static let returnUserErrorCode = "error_code"
128128
public static let returnUserOldATB = "old_atb"
129129
public static let returnUserNewATB = "new_atb"
130+
131+
// Pixel Experiment
132+
public static let cohort = "cohort"
130133
}
131134

132135
public struct PixelValues {

Core/PixelEvent.swift

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ extension Pixel {
6868
case browsingMenuShare
6969
case browsingMenuCopy
7070
case browsingMenuPrint
71-
case browsingMenuSettings
7271
case browsingMenuFindInPage
7372
case browsingMenuDisableProtection
7473
case browsingMenuEnableProtection
@@ -100,8 +99,6 @@ extension Pixel {
10099
case homeScreenEditFavorite
101100
case homeScreenDeleteFavorite
102101

103-
case autocompleteEnabled
104-
case autocompleteDisabled
105102
case autocompleteClickPhrase
106103
case autocompleteClickWebsite
107104
case autocompleteClickBookmark
@@ -124,8 +121,6 @@ extension Pixel {
124121
case daxDialogsFireEducationConfirmed
125122
case daxDialogsFireEducationCancelled
126123

127-
case defaultBrowserButtonPressedSettings
128-
129124
case widgetsOnboardingCTAPressed
130125
case widgetsOnboardingDeclineOptionPressed
131126
case widgetsOnboardingMovedToBackground
@@ -150,8 +145,7 @@ extension Pixel {
150145
case bookmarkImportFailureUnknown
151146
case bookmarkExportSuccess
152147
case bookmarkExportFailure
153-
154-
case textSizeSettingsShown
148+
155149
case textSizeSettingsChanged
156150

157151
case downloadStarted
@@ -596,7 +590,48 @@ extension Pixel {
596590
case privacyProVPNAccessRevokedDialogShown
597591
case privacyProVPNBetaStoppedWhenPrivacyProEnabled
598592

599-
// Full site address setting
593+
// MARK: Pixel Experiment
594+
case pixelExperimentEnrollment
595+
case settingsPresented
596+
case settingsSetAsDefault
597+
case settingsPrivateSearchOpen
598+
case settingsPrivateSearchAutocompleteOn
599+
case settingsPrivateSearchAutocompleteOff
600+
case settingsVoiceSearchOn
601+
case settingsVoiceSearchOff
602+
case settingsPrivateSearchVoiceSearchOn
603+
case settingsPrivateSearchVoiceSearchOff
604+
case settingsWebTrackingProtectionOpen
605+
case settingsGpcOn
606+
case settingsGpcOff
607+
case settingsEmailProtectionOpen
608+
case settingsEmailProtectionLearnMore
609+
case settingsGeneralOpen
610+
case settingsAutocompleteOn
611+
case settingsAutocompleteOff
612+
case settingsGeneralAutocompleteOn
613+
case settingsGeneralAutocompleteOff
614+
case settingsGeneralVoiceSearchOn
615+
case settingsGeneralVoiceSearchOff
616+
case settingsSyncOpen
617+
case settingsAppearanceOpen
618+
case settingsAddressBarSelectorPressed
619+
case settingsAddressBarTopSelected
620+
case settingsAddressBarBottomSelected
621+
case settingsIconSelectorPressed
622+
case settingsThemeSelectorPressed
623+
case settingsAccessibilityOpen
624+
case settingsAccessiblityTextSize
625+
case settingsAccessibilityVoiceSearchOn
626+
case settingsAccessibilityVoiceSearchOff
627+
case settingsDataClearingOpen
628+
case settingsFireButtonSelectorPressed
629+
case settingsDataClearingClearDataOpen
630+
case settingsAutomaticallyClearDataOpen
631+
case settingsAutomaticallyClearDataOn
632+
case settingsAutomaticallyClearDataOff
633+
case settingsNextStepsAddAppToDock
634+
case settingsNextStepsAddWidget
600635
case settingsShowFullSiteAddressEnabled
601636
case settingsShowFullSiteAddressDisabled
602637

@@ -653,7 +688,6 @@ extension Pixel.Event {
653688
case .browsingMenuToggleBrowsingMode: return "mb_dm"
654689
case .browsingMenuCopy: return "mb_cp"
655690
case .browsingMenuPrint: return "mb_pr"
656-
case .browsingMenuSettings: return "mb_st"
657691
case .browsingMenuFindInPage: return "mb_fp"
658692
case .browsingMenuDisableProtection: return "mb_wla"
659693
case .browsingMenuEnableProtection: return "mb_wlr"
@@ -686,8 +720,6 @@ extension Pixel.Event {
686720
case .homeScreenEditFavorite: return "mh_ef"
687721
case .homeScreenDeleteFavorite: return "mh_df"
688722

689-
case .autocompleteEnabled: return "m_autocomplete_toggled_on"
690-
case .autocompleteDisabled: return "m_autocomplete_toggled_off"
691723
case .autocompleteClickPhrase: return "m_autocomplete_click_phrase"
692724
case .autocompleteClickWebsite: return "m_autocomplete_click_website"
693725
case .autocompleteClickBookmark: return "m_autocomplete_click_bookmark"
@@ -710,8 +742,6 @@ extension Pixel.Event {
710742
case .daxDialogsFireEducationConfirmed: return "m_dx_fe_co"
711743
case .daxDialogsFireEducationCancelled: return "m_dx_fe_ca"
712744

713-
case .defaultBrowserButtonPressedSettings: return "m_db_s"
714-
715745
case .widgetsOnboardingCTAPressed: return "m_o_w_a"
716746
case .widgetsOnboardingDeclineOptionPressed: return "m_o_w_d"
717747
case .widgetsOnboardingMovedToBackground: return "m_o_w_b"
@@ -736,8 +766,7 @@ extension Pixel.Event {
736766
case .bookmarkImportFailureUnknown: return "m_bi_e_unknown"
737767
case .bookmarkExportSuccess: return "m_be_a"
738768
case .bookmarkExportFailure: return "m_be_e"
739-
740-
case .textSizeSettingsShown: return "m_text_size_settings_shown"
769+
741770
case .textSizeSettingsChanged: return "m_text_size_settings_changed"
742771

743772
case .downloadStarted: return "m_download_started"
@@ -1169,14 +1198,59 @@ extension Pixel.Event {
11691198
case .privacyProSubscriptionManagementEmail: return "m_privacy-pro_manage-email_edit_click"
11701199
case .privacyProSubscriptionManagementPlanBilling: return "m_privacy-pro_settings_change-plan-or-billing_click"
11711200
case .privacyProSubscriptionManagementRemoval: return "m_privacy-pro_settings_remove-from-device_click"
1201+
1202+
// MARK: Pixel Experiment
1203+
case .pixelExperimentEnrollment: return "pixel_experiment_enrollment"
1204+
case .settingsPresented: return "m_settings_presented"
1205+
case .settingsSetAsDefault: return "m_settings_set_as_default"
1206+
case .settingsPrivateSearchOpen: return "m_settings_private_search_open"
1207+
case .settingsPrivateSearchAutocompleteOn: return "m_settings_private_search_autocomplete_on"
1208+
case .settingsPrivateSearchAutocompleteOff: return "m_settings_private_search_autocomplete_off"
1209+
case .settingsVoiceSearchOn: return "m_settings_voice_search_on"
1210+
case .settingsVoiceSearchOff: return "m_settings_voice_search_off"
1211+
case .settingsPrivateSearchVoiceSearchOn: return "m_settings_private_search_voice_search_on"
1212+
case .settingsPrivateSearchVoiceSearchOff: return "m_settings_private_search_voice_search_off"
1213+
case .settingsWebTrackingProtectionOpen: return "m_settings_web_tracking_protection_open"
1214+
case .settingsGpcOn: return "m_settings_gpc_on"
1215+
case .settingsGpcOff: return "m_settings_gpc_off"
1216+
case .settingsEmailProtectionOpen: return "m_settings_email_protection_open"
1217+
case .settingsEmailProtectionLearnMore: return "m_settings_email_protection_learn_more"
1218+
case .settingsGeneralOpen: return "m_settings_general_open"
1219+
case .settingsAutocompleteOn: return "m_settings_autocomplete_on"
1220+
case .settingsAutocompleteOff: return "m_settings_autocomplete_off"
1221+
case .settingsGeneralAutocompleteOn: return "m_settings_general_autocomplete_on"
1222+
case .settingsGeneralAutocompleteOff: return "m_settings_general_autocomplete_off"
1223+
case .settingsGeneralVoiceSearchOn: return "m_settings_general_voice_search_on"
1224+
case .settingsGeneralVoiceSearchOff: return "m_settings_general_voice_search_off"
1225+
case .settingsSyncOpen: return "m_settings_sync_open"
1226+
case .settingsAppearanceOpen: return "m_settings_appearance_open"
1227+
case .settingsAddressBarSelectorPressed: return "m_settings_address_bar_selector_pressed"
1228+
case .settingsAddressBarTopSelected: return "m_settings_address_bar_top_selected"
1229+
case .settingsAddressBarBottomSelected: return "m_settings_address_bar_bottom_selected"
1230+
case .settingsIconSelectorPressed: return "m_settings_icon_selector_pressed"
1231+
case .settingsThemeSelectorPressed: return "m_settings_theme_selector_pressed"
1232+
case .settingsAccessibilityOpen: return "m_settings_accessibility_open"
1233+
case .settingsAccessiblityTextSize: return "m_settings_accessiblity_text_size"
1234+
case .settingsAccessibilityVoiceSearchOn: return "m_settings_accessibility_voice_search_on"
1235+
case .settingsAccessibilityVoiceSearchOff: return "m_settings_accessibility_voice_search_off"
1236+
case .settingsDataClearingOpen: return "m_settings_data_clearing_open"
1237+
case .settingsFireButtonSelectorPressed: return "m_settings_fire_button_selector_pressed"
1238+
case .settingsDataClearingClearDataOpen: return "m_settings_data_clearing_clear_data_open"
1239+
case .settingsAutomaticallyClearDataOpen: return "m_settings_data_clearing_clear_data_open"
1240+
case .settingsAutomaticallyClearDataOn: return "m_settings_automatically_clear_data_on"
1241+
case .settingsAutomaticallyClearDataOff: return "m_settings_automatically_clear_data_off"
1242+
case .settingsNextStepsAddAppToDock: return "m_settings_next_steps_add_app_to_dock"
1243+
case .settingsNextStepsAddWidget: return "m_settings_next_steps_add_widget"
11721244
case .settingsShowFullSiteAddressEnabled: return "m_settings_show_full_url_on"
11731245
case .settingsShowFullSiteAddressDisabled: return "m_settings_show_full_url_off"
1174-
// Launch
1246+
1247+
// Launch
11751248
case .privacyProFeatureEnabled: return "m_privacy-pro_feature_enabled"
11761249
case .privacyProPromotionDialogShownVPN: return "m_privacy-pro_promotion-dialog_shown_vpn"
11771250
case .privacyProVPNAccessRevokedDialogShown: return "m_privacy-pro_vpn-access-revoked-dialog_shown"
11781251
case .privacyProVPNBetaStoppedWhenPrivacyProEnabled: return "m_privacy-pro_vpn-beta-stopped-when-privacy-pro-enabled"
1179-
// Web
1252+
1253+
// Web
11801254
case .privacyProOfferMonthlyPriceClick: return "m_privacy-pro_offer_monthly-price_click"
11811255
case .privacyProOfferYearlyPriceClick: return "m_privacy-pro_offer_yearly-price_click"
11821256
case .privacyProAddEmailSuccess: return "m_privacy-pro_app_add-email_success_u"

Core/PixelExperiment.swift

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//
2+
// PixelExperiment.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2024 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
public enum PixelExperiment: String, CaseIterable {
23+
24+
fileprivate static var logic: PixelExperimentLogic {
25+
customLogic ?? defaultLogic
26+
}
27+
fileprivate static let defaultLogic = PixelExperimentLogic {
28+
Pixel.fire(pixel: $0,
29+
withAdditionalParameters: PixelExperiment.parameters)
30+
}
31+
// Custom logic for testing purposes
32+
static var customLogic: PixelExperimentLogic?
33+
34+
/// When `cohort` is accessed for the first time after the experiment is installed with `install()`,
35+
/// allocate and return a cohort. Subsequently, return the same cohort.
36+
public static var cohort: PixelExperiment? {
37+
logic.cohort
38+
}
39+
40+
static var isExperimentInstalled: Bool {
41+
return logic.isInstalled
42+
}
43+
44+
static var allocatedCohortDoesNotMatchCurrentCohorts: Bool {
45+
guard let allocatedCohort = logic.allocatedCohort else { return false }
46+
if PixelExperiment(rawValue: allocatedCohort) == nil {
47+
return true
48+
}
49+
return false
50+
}
51+
52+
/// Enables this experiment for new users when called from the new installation path.
53+
public static func install() {
54+
// Disable the experiment until all other experiments are finished
55+
logic.install()
56+
}
57+
58+
static func cleanup() {
59+
logic.cleanup()
60+
}
61+
62+
// These are the variants. Rename or add/remove them as needed. If you change the string value
63+
// remember to keep it clear for privacy triage.
64+
case control
65+
case newSettings
66+
67+
// Internal state for users not included in any variant
68+
case noVariant
69+
}
70+
71+
extension PixelExperiment {
72+
73+
// Pixel parameter - cohort
74+
public static var parameters: [String: String] {
75+
guard let cohort, cohort != .noVariant else {
76+
return [:]
77+
}
78+
79+
return [PixelParameters.cohort: cohort.rawValue]
80+
}
81+
82+
}
83+
84+
final internal class PixelExperimentLogic {
85+
86+
var cohort: PixelExperiment? {
87+
guard isInstalled else { return nil }
88+
89+
// Use the `customCohort` if it's set
90+
if let customCohort = customCohort {
91+
return customCohort
92+
}
93+
94+
// Check if a cohort is already allocated and valid
95+
if let allocatedCohort,
96+
let cohort = PixelExperiment(rawValue: allocatedCohort) {
97+
return cohort
98+
}
99+
100+
let randomNumber = Int.random(in: 0..<100)
101+
102+
// Allocate user to a cohort based on the random number
103+
let cohort: PixelExperiment
104+
if randomNumber < 5 {
105+
cohort = .control
106+
} else if randomNumber < 10 {
107+
cohort = .newSettings
108+
} else {
109+
cohort = .noVariant
110+
}
111+
112+
// Store and use the selected cohort
113+
allocatedCohort = cohort.rawValue
114+
fireEnrollmentPixel()
115+
return cohort
116+
}
117+
118+
@UserDefaultsWrapper(key: .pixelExperimentInstalled, defaultValue: false)
119+
var isInstalled: Bool
120+
121+
@UserDefaultsWrapper(key: .pixelExperimentCohort, defaultValue: nil)
122+
var allocatedCohort: String?
123+
124+
private let fire: (Pixel.Event) -> Void
125+
private let customCohort: PixelExperiment?
126+
127+
init(fire: @escaping (Pixel.Event) -> Void,
128+
customCohort: PixelExperiment? = nil) {
129+
self.fire = fire
130+
self.customCohort = customCohort
131+
}
132+
133+
func install() {
134+
isInstalled = true
135+
}
136+
137+
private func fireEnrollmentPixel() {
138+
guard cohort != .noVariant else {
139+
return
140+
}
141+
142+
fire(.pixelExperimentEnrollment)
143+
}
144+
145+
func cleanup() {
146+
isInstalled = false
147+
allocatedCohort = nil
148+
}
149+
150+
}

Core/UserDefaultsPropertyWrapper.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public struct UserDefaultsWrapper<T> {
127127
case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp"
128128
case didBurnTimestamp = "com.duckduckgo.ios.userBehavior.didBurnTimestamp"
129129

130+
case pixelExperimentInstalled = "com.duckduckgo.ios.pixel.experiment.installed"
131+
case pixelExperimentCohort = "com.duckduckgo.ios.pixel.experiment.cohort"
132+
case pixelExperimentEnrollmentDate = "com.duckduckgo.ios.pixel.experiment.enrollment.date"
130133
}
131134

132135
private let key: Key

0 commit comments

Comments
 (0)