Skip to content

Commit

Permalink
Updated settings (#2603)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tomasstrba authored Apr 9, 2024
1 parent 2b0a8dc commit 9a9cd02
Show file tree
Hide file tree
Showing 219 changed files with 16,915 additions and 2,049 deletions.
9 changes: 9 additions & 0 deletions Core/AppURLs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import BrowserServicesKit
import Foundation

// swiftlint:disable line_length

public extension URL {

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

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

Expand Down Expand Up @@ -262,3 +269,5 @@ public final class StatisticsDependentURLFactory {
}

}

// swiftlint:enable line_length
3 changes: 3 additions & 0 deletions Core/Pixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public struct PixelParameters {
public static let returnUserErrorCode = "error_code"
public static let returnUserOldATB = "old_atb"
public static let returnUserNewATB = "new_atb"

// Pixel Experiment
public static let cohort = "cohort"
}

public struct PixelValues {
Expand Down
108 changes: 91 additions & 17 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ extension Pixel {
case browsingMenuShare
case browsingMenuCopy
case browsingMenuPrint
case browsingMenuSettings
case browsingMenuFindInPage
case browsingMenuDisableProtection
case browsingMenuEnableProtection
Expand Down Expand Up @@ -100,8 +99,6 @@ extension Pixel {
case homeScreenEditFavorite
case homeScreenDeleteFavorite

case autocompleteEnabled
case autocompleteDisabled
case autocompleteClickPhrase
case autocompleteClickWebsite
case autocompleteClickBookmark
Expand All @@ -124,8 +121,6 @@ extension Pixel {
case daxDialogsFireEducationConfirmed
case daxDialogsFireEducationCancelled

case defaultBrowserButtonPressedSettings

case widgetsOnboardingCTAPressed
case widgetsOnboardingDeclineOptionPressed
case widgetsOnboardingMovedToBackground
Expand All @@ -150,8 +145,7 @@ extension Pixel {
case bookmarkImportFailureUnknown
case bookmarkExportSuccess
case bookmarkExportFailure

case textSizeSettingsShown

case textSizeSettingsChanged

case downloadStarted
Expand Down Expand Up @@ -596,7 +590,48 @@ extension Pixel {
case privacyProVPNAccessRevokedDialogShown
case privacyProVPNBetaStoppedWhenPrivacyProEnabled

// Full site address setting
// MARK: Pixel Experiment
case pixelExperimentEnrollment
case settingsPresented
case settingsSetAsDefault
case settingsPrivateSearchOpen
case settingsPrivateSearchAutocompleteOn
case settingsPrivateSearchAutocompleteOff
case settingsVoiceSearchOn
case settingsVoiceSearchOff
case settingsPrivateSearchVoiceSearchOn
case settingsPrivateSearchVoiceSearchOff
case settingsWebTrackingProtectionOpen
case settingsGpcOn
case settingsGpcOff
case settingsEmailProtectionOpen
case settingsEmailProtectionLearnMore
case settingsGeneralOpen
case settingsAutocompleteOn
case settingsAutocompleteOff
case settingsGeneralAutocompleteOn
case settingsGeneralAutocompleteOff
case settingsGeneralVoiceSearchOn
case settingsGeneralVoiceSearchOff
case settingsSyncOpen
case settingsAppearanceOpen
case settingsAddressBarSelectorPressed
case settingsAddressBarTopSelected
case settingsAddressBarBottomSelected
case settingsIconSelectorPressed
case settingsThemeSelectorPressed
case settingsAccessibilityOpen
case settingsAccessiblityTextSize
case settingsAccessibilityVoiceSearchOn
case settingsAccessibilityVoiceSearchOff
case settingsDataClearingOpen
case settingsFireButtonSelectorPressed
case settingsDataClearingClearDataOpen
case settingsAutomaticallyClearDataOpen
case settingsAutomaticallyClearDataOn
case settingsAutomaticallyClearDataOff
case settingsNextStepsAddAppToDock
case settingsNextStepsAddWidget
case settingsShowFullSiteAddressEnabled
case settingsShowFullSiteAddressDisabled

Expand Down Expand Up @@ -653,7 +688,6 @@ extension Pixel.Event {
case .browsingMenuToggleBrowsingMode: return "mb_dm"
case .browsingMenuCopy: return "mb_cp"
case .browsingMenuPrint: return "mb_pr"
case .browsingMenuSettings: return "mb_st"
case .browsingMenuFindInPage: return "mb_fp"
case .browsingMenuDisableProtection: return "mb_wla"
case .browsingMenuEnableProtection: return "mb_wlr"
Expand Down Expand Up @@ -686,8 +720,6 @@ extension Pixel.Event {
case .homeScreenEditFavorite: return "mh_ef"
case .homeScreenDeleteFavorite: return "mh_df"

case .autocompleteEnabled: return "m_autocomplete_toggled_on"
case .autocompleteDisabled: return "m_autocomplete_toggled_off"
case .autocompleteClickPhrase: return "m_autocomplete_click_phrase"
case .autocompleteClickWebsite: return "m_autocomplete_click_website"
case .autocompleteClickBookmark: return "m_autocomplete_click_bookmark"
Expand All @@ -710,8 +742,6 @@ extension Pixel.Event {
case .daxDialogsFireEducationConfirmed: return "m_dx_fe_co"
case .daxDialogsFireEducationCancelled: return "m_dx_fe_ca"

case .defaultBrowserButtonPressedSettings: return "m_db_s"

case .widgetsOnboardingCTAPressed: return "m_o_w_a"
case .widgetsOnboardingDeclineOptionPressed: return "m_o_w_d"
case .widgetsOnboardingMovedToBackground: return "m_o_w_b"
Expand All @@ -736,8 +766,7 @@ extension Pixel.Event {
case .bookmarkImportFailureUnknown: return "m_bi_e_unknown"
case .bookmarkExportSuccess: return "m_be_a"
case .bookmarkExportFailure: return "m_be_e"

case .textSizeSettingsShown: return "m_text_size_settings_shown"

case .textSizeSettingsChanged: return "m_text_size_settings_changed"

case .downloadStarted: return "m_download_started"
Expand Down Expand Up @@ -1169,14 +1198,59 @@ extension Pixel.Event {
case .privacyProSubscriptionManagementEmail: return "m_privacy-pro_manage-email_edit_click"
case .privacyProSubscriptionManagementPlanBilling: return "m_privacy-pro_settings_change-plan-or-billing_click"
case .privacyProSubscriptionManagementRemoval: return "m_privacy-pro_settings_remove-from-device_click"

// MARK: Pixel Experiment
case .pixelExperimentEnrollment: return "pixel_experiment_enrollment"
case .settingsPresented: return "m_settings_presented"
case .settingsSetAsDefault: return "m_settings_set_as_default"
case .settingsPrivateSearchOpen: return "m_settings_private_search_open"
case .settingsPrivateSearchAutocompleteOn: return "m_settings_private_search_autocomplete_on"
case .settingsPrivateSearchAutocompleteOff: return "m_settings_private_search_autocomplete_off"
case .settingsVoiceSearchOn: return "m_settings_voice_search_on"
case .settingsVoiceSearchOff: return "m_settings_voice_search_off"
case .settingsPrivateSearchVoiceSearchOn: return "m_settings_private_search_voice_search_on"
case .settingsPrivateSearchVoiceSearchOff: return "m_settings_private_search_voice_search_off"
case .settingsWebTrackingProtectionOpen: return "m_settings_web_tracking_protection_open"
case .settingsGpcOn: return "m_settings_gpc_on"
case .settingsGpcOff: return "m_settings_gpc_off"
case .settingsEmailProtectionOpen: return "m_settings_email_protection_open"
case .settingsEmailProtectionLearnMore: return "m_settings_email_protection_learn_more"
case .settingsGeneralOpen: return "m_settings_general_open"
case .settingsAutocompleteOn: return "m_settings_autocomplete_on"
case .settingsAutocompleteOff: return "m_settings_autocomplete_off"
case .settingsGeneralAutocompleteOn: return "m_settings_general_autocomplete_on"
case .settingsGeneralAutocompleteOff: return "m_settings_general_autocomplete_off"
case .settingsGeneralVoiceSearchOn: return "m_settings_general_voice_search_on"
case .settingsGeneralVoiceSearchOff: return "m_settings_general_voice_search_off"
case .settingsSyncOpen: return "m_settings_sync_open"
case .settingsAppearanceOpen: return "m_settings_appearance_open"
case .settingsAddressBarSelectorPressed: return "m_settings_address_bar_selector_pressed"
case .settingsAddressBarTopSelected: return "m_settings_address_bar_top_selected"
case .settingsAddressBarBottomSelected: return "m_settings_address_bar_bottom_selected"
case .settingsIconSelectorPressed: return "m_settings_icon_selector_pressed"
case .settingsThemeSelectorPressed: return "m_settings_theme_selector_pressed"
case .settingsAccessibilityOpen: return "m_settings_accessibility_open"
case .settingsAccessiblityTextSize: return "m_settings_accessiblity_text_size"
case .settingsAccessibilityVoiceSearchOn: return "m_settings_accessibility_voice_search_on"
case .settingsAccessibilityVoiceSearchOff: return "m_settings_accessibility_voice_search_off"
case .settingsDataClearingOpen: return "m_settings_data_clearing_open"
case .settingsFireButtonSelectorPressed: return "m_settings_fire_button_selector_pressed"
case .settingsDataClearingClearDataOpen: return "m_settings_data_clearing_clear_data_open"
case .settingsAutomaticallyClearDataOpen: return "m_settings_data_clearing_clear_data_open"
case .settingsAutomaticallyClearDataOn: return "m_settings_automatically_clear_data_on"
case .settingsAutomaticallyClearDataOff: return "m_settings_automatically_clear_data_off"
case .settingsNextStepsAddAppToDock: return "m_settings_next_steps_add_app_to_dock"
case .settingsNextStepsAddWidget: return "m_settings_next_steps_add_widget"
case .settingsShowFullSiteAddressEnabled: return "m_settings_show_full_url_on"
case .settingsShowFullSiteAddressDisabled: return "m_settings_show_full_url_off"
// Launch

// Launch
case .privacyProFeatureEnabled: return "m_privacy-pro_feature_enabled"
case .privacyProPromotionDialogShownVPN: return "m_privacy-pro_promotion-dialog_shown_vpn"
case .privacyProVPNAccessRevokedDialogShown: return "m_privacy-pro_vpn-access-revoked-dialog_shown"
case .privacyProVPNBetaStoppedWhenPrivacyProEnabled: return "m_privacy-pro_vpn-beta-stopped-when-privacy-pro-enabled"
// Web

// Web
case .privacyProOfferMonthlyPriceClick: return "m_privacy-pro_offer_monthly-price_click"
case .privacyProOfferYearlyPriceClick: return "m_privacy-pro_offer_yearly-price_click"
case .privacyProAddEmailSuccess: return "m_privacy-pro_app_add-email_success_u"
Expand Down
150 changes: 150 additions & 0 deletions Core/PixelExperiment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// PixelExperiment.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 enum PixelExperiment: String, CaseIterable {

fileprivate static var logic: PixelExperimentLogic {
customLogic ?? defaultLogic
}
fileprivate static let defaultLogic = PixelExperimentLogic {
Pixel.fire(pixel: $0,
withAdditionalParameters: PixelExperiment.parameters)
}
// Custom logic for testing purposes
static var customLogic: PixelExperimentLogic?

/// When `cohort` is accessed for the first time after the experiment is installed with `install()`,
/// allocate and return a cohort. Subsequently, return the same cohort.
public static var cohort: PixelExperiment? {
logic.cohort
}

static var isExperimentInstalled: Bool {
return logic.isInstalled
}

static var allocatedCohortDoesNotMatchCurrentCohorts: Bool {
guard let allocatedCohort = logic.allocatedCohort else { return false }
if PixelExperiment(rawValue: allocatedCohort) == nil {
return true
}
return false
}

/// Enables this experiment for new users when called from the new installation path.
public static func install() {
// Disable the experiment until all other experiments are finished
logic.install()
}

static func cleanup() {
logic.cleanup()
}

// These are the variants. Rename or add/remove them as needed. If you change the string value
// remember to keep it clear for privacy triage.
case control
case newSettings

// Internal state for users not included in any variant
case noVariant
}

extension PixelExperiment {

// Pixel parameter - cohort
public static var parameters: [String: String] {
guard let cohort, cohort != .noVariant else {
return [:]
}

return [PixelParameters.cohort: cohort.rawValue]
}

}

final internal class PixelExperimentLogic {

var cohort: PixelExperiment? {
guard isInstalled else { return nil }

// Use the `customCohort` if it's set
if let customCohort = customCohort {
return customCohort
}

// Check if a cohort is already allocated and valid
if let allocatedCohort,
let cohort = PixelExperiment(rawValue: allocatedCohort) {
return cohort
}

let randomNumber = Int.random(in: 0..<100)

// Allocate user to a cohort based on the random number
let cohort: PixelExperiment
if randomNumber < 5 {
cohort = .control
} else if randomNumber < 10 {
cohort = .newSettings
} else {
cohort = .noVariant
}

// Store and use the selected cohort
allocatedCohort = cohort.rawValue
fireEnrollmentPixel()
return cohort
}

@UserDefaultsWrapper(key: .pixelExperimentInstalled, defaultValue: false)
var isInstalled: Bool

@UserDefaultsWrapper(key: .pixelExperimentCohort, defaultValue: nil)
var allocatedCohort: String?

private let fire: (Pixel.Event) -> Void
private let customCohort: PixelExperiment?

init(fire: @escaping (Pixel.Event) -> Void,
customCohort: PixelExperiment? = nil) {
self.fire = fire
self.customCohort = customCohort
}

func install() {
isInstalled = true
}

private func fireEnrollmentPixel() {
guard cohort != .noVariant else {
return
}

fire(.pixelExperimentEnrollment)
}

func cleanup() {
isInstalled = false
allocatedCohort = nil
}

}
3 changes: 3 additions & 0 deletions Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public struct UserDefaultsWrapper<T> {
case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp"
case didBurnTimestamp = "com.duckduckgo.ios.userBehavior.didBurnTimestamp"

case pixelExperimentInstalled = "com.duckduckgo.ios.pixel.experiment.installed"
case pixelExperimentCohort = "com.duckduckgo.ios.pixel.experiment.cohort"
case pixelExperimentEnrollmentDate = "com.duckduckgo.ios.pixel.experiment.enrollment.date"
}

private let key: Key
Expand Down
Loading

0 comments on commit 9a9cd02

Please sign in to comment.