Skip to content

Commit

Permalink
Merge branch 'main' into sam/set-vpn-user-agent
Browse files Browse the repository at this point in the history
# By Christopher Brind (4) and others
# Via Alessandro Boron (1) and others
* main: (27 commits)
  Bump C-S-S to 6.14.1 (#3331)
  DuckPlayer Launch Experiment for iOS (#3328)
  defer loading the tab switcher button until view did load (#3326)
  Release 7.136.0-3 (#3324)
  Release PR: Check for the negative attribution case (#3311)
  fix tab switcher crashes (speculative fix) (#3319)
  Onboarding highlights feature flag setup (#3308)
  Release 7.136.0-2 (#3320)
  Attempt to fix dissapearing privacy icon (#3317)
  Fix bookmarks toolbar behaviour with Sync Promo on iOS 15 (#3313)
  Bump BSK with C-S-S to 6.14.0 (#3314)
  ensure no atb or app version sent with pixel (#3315)
  Fix #3298: Add support for Xcode 16 (#3299)
  Fix Keychain Debug view controller segue (#3310)
  Release 7.136.0-1 (#3309)
  Fix an issue that causes the fire dialog to show multiple times for the same website after dismissing it (#3305)
  [DuckPlayer] 28. Open in Youtube -> Youtube App (#3290)
  usage segmentation (#3263)
  Fix wrong URL displayed for auth dialog (#3307)
  iOS Integration of BSK Onboarding (#3282)
  ...

# Conflicts:
#	DuckDuckGo.xcodeproj/project.pbxproj
#	DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Loading branch information
samsymons committed Sep 9, 2024
2 parents b41093b + 75733ad commit a436229
Show file tree
Hide file tree
Showing 172 changed files with 6,991 additions and 2,084 deletions.
5 changes: 5 additions & 0 deletions .maestro/shared/sync_login.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ appId: com.duckduckgo.mobile.ios
- inputText: "0000"
- pressKey: Enter
- assertVisible: Scan QR Code
- runFlow:
when:
visible: Allows you to upload photographs and videos
commands:
- tapOn: "OK"
- tapOn: Manually Enter Code
- tapOn: Paste
- assertVisible: Save Recovery Code
Expand Down
5 changes: 5 additions & 0 deletions .maestro/sync_tests/06_delete_account.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ name: 06_delete_account
- inputText: "0000"
- pressKey: Enter
- assertVisible: Scan QR Code
- runFlow:
when:
visible: Allows you to upload photographs and videos
commands:
- tapOn: "OK"
- tapOn: Manually Enter Code
- tapOn: Paste
- assertVisible: Sync & Backup Error
Expand Down
2 changes: 1 addition & 1 deletion Configuration/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MARKETING_VERSION = 7.135.0
MARKETING_VERSION = 7.136.0
4 changes: 2 additions & 2 deletions Core/AppPrivacyConfigurationDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import BrowserServicesKit
final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider {

public struct Constants {
public static let embeddedDataETag = "\"3310d00f227a02b53a16ed4af90afee5\""
public static let embeddedDataSHA = "7371e7fd3293e42638aa3bd3905a205e70827b2fd980848af108c82cedabf8be"
public static let embeddedDataETag = "\"38047fbabac1af7f77112ee692d5d481\""
public static let embeddedDataSHA = "ee998861bbed8b784a7f19caafd76a8f6eb9a82160b0b6ddb21d97e67332b38f"
}

public var embeddedDataEtag: String {
Expand Down
98 changes: 97 additions & 1 deletion Core/Atb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,105 @@

import Foundation

public struct Atb: Decodable {
public struct Atb: Decodable, Equatable {

/// Format is v<week>-<day>
/// * day is `1...7` with 1 being Wednesday
/// * note that week is NOT padded but ATBs older than week 100 should never be seen by the apps, ie no one has this installed before Feb 2018 and week 99 is Jan 2018
/// * ATBs > 999 would be about 10 years in the future (Apr 2035), we can fix it nearer the time
static let template = "v100-1"

/// Same as `template` two characters on the end, e.g. `ma`
static let templateWithVariant = template + "xx"

let version: String
let updateVersion: String?
let numeric: AtbNumeric?

init(version: String, updateVersion: String?) {
self.version = version
self.updateVersion = updateVersion
self.numeric = AtbNumeric.makeFromVersion(version)
}

enum CodingKeys: CodingKey {
case version
case updateVersion
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.updateVersion = try container.decodeIfPresent(String.self, forKey: .updateVersion)
self.numeric = AtbNumeric.makeFromVersion(version)
}

/// Equality is about the version without any variants. e.g. v100-1 == v100-1ma. `updateVersion` is ignored because that's a signal from the server to update the locally stored Atb so not relevant to any calculation
public static func == (lhs: Atb, rhs: Atb) -> Bool {
return lhs.droppingVariant == rhs.droppingVariant
}

/// Subtracts one ATB from the other.
/// @return difference in days
public static func - (lhs: Atb, rhs: Atb) -> Int {
return lhs.ageInDays - rhs.ageInDays
}

/// Gives age in days since first ATB. If badly formatted returns -1. Only the server should be giving us ATB values, so if it is giving us something wrong there are bigger problems in the world.
var ageInDays: Int {
numeric?.ageInDays ?? -1
}

/// Gives the current week or -1 if badly formatted
var week: Int {
numeric?.week ?? -1
}

var isReturningUser: Bool {
version.count == Self.templateWithVariant.count && version.hasSuffix("ru")
}

struct AtbNumeric {

let week: Int
let day: Int
let ageInDays: Int

static func makeFromVersion(_ version: String) -> AtbNumeric? {
let version = String(version.prefix(Atb.template.count))
guard version.count == Atb.template.count,
let week = Int(version.substring(1...3)),
let day = Int(version.substring(5...5)),
(1...7).contains(day) else {

if !ProcessInfo().arguments.contains("testing") {
assertionFailure("bad atb")
}
return nil
}

return AtbNumeric(week: week, day: day, ageInDays: (week * 7) + (day - 1))
}

}

}

extension Atb {

var droppingVariant: String {
return String(version.prefix(Atb.template.count))
}

}

private extension String {

func substring(_ range: ClosedRange<Int>) -> String {
let startIndex = self.index(self.startIndex, offsetBy: range.lowerBound)
let endIndex = self.index(self.startIndex, offsetBy: min(self.count, range.upperBound + 1))
let substring = self[startIndex..<endIndex]
return String(substring)
}

}
8 changes: 7 additions & 1 deletion Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum FeatureFlag: String {
case autofillOnByDefault
case autofillFailureReporting
case autofillOnForExistingUsers
case autofillUnknownUsernameCategorization
case incontextSignup
case autoconsentOnByDefault
case history
Expand All @@ -39,6 +40,7 @@ public enum FeatureFlag: String {
case sslCertificatesBypass
case syncPromotionBookmarks
case syncPromotionPasswords
case onboardingHighlights
}

extension FeatureFlag: FeatureFlagSourceProviding {
Expand All @@ -64,14 +66,16 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .remoteReleasable(.feature(.autofillBreakageReporter))
case .autofillOnForExistingUsers:
return .remoteReleasable(.subfeature(AutofillSubfeature.onForExistingUsers))
case .autofillUnknownUsernameCategorization:
return .remoteReleasable(.subfeature(AutofillSubfeature.unknownUsernameCategorization))
case .incontextSignup:
return .remoteReleasable(.feature(.incontextSignup))
case .autoconsentOnByDefault:
return .remoteReleasable(.subfeature(AutoconsentSubfeature.onByDefault))
case .history:
return .remoteReleasable(.feature(.history))
case .newTabPageSections:
return .internalOnly
return .remoteDevelopment(.feature(.newTabPageImprovements))
case .duckPlayer:
return .remoteReleasable(.feature(.duckPlayer))
case .sslCertificatesBypass:
Expand All @@ -80,6 +84,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .remoteReleasable(.subfeature(SyncPromotionSubfeature.bookmarks))
case .syncPromotionPasswords:
return .remoteReleasable(.subfeature(SyncPromotionSubfeature.passwords))
case .onboardingHighlights:
return .internalOnly
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ extension Pixel {

// MARK: Apple Ad Attribution
case appleAdAttribution
case appleAdAttributionNotAttributed

// MARK: Secure Vault
case secureVaultL1KeyMigration
Expand Down Expand Up @@ -787,6 +788,9 @@ extension Pixel {
case duckPlayerContingencySettingsDisplayed
case duckPlayerContingencyLearnMoreClicked

// MARK: enhanced statistics
case usageSegments

// MARK: Certificate warnings
case certificateWarningDisplayed(_ errorType: String)
case certificateWarningLeaveClicked
Expand All @@ -803,6 +807,13 @@ extension Pixel {
case pproFeedbackSubcategoryScreenShow(source: String, reportType: String, category: String)
case pproFeedbackSubmitScreenShow(source: String, reportType: String, category: String, subcategory: String)
case pproFeedbackSubmitScreenFAQClick(source: String, reportType: String, category: String, subcategory: String)

// MARK: DuckPlayer Pixel Experiment
case duckplayerExperimentCohortAssign
case duckplayerExperimentSearch
case duckplayerExperimentDailySearch
case duckplayerExperimentWeeklySearch
case duckplayerExperimentYoutubePageView
}

}
Expand Down Expand Up @@ -1431,6 +1442,7 @@ 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"
Expand Down Expand Up @@ -1583,6 +1595,9 @@ extension Pixel.Event {
case .duckPlayerContingencySettingsDisplayed: return "duckplayer_ios_contingency_settings-displayed"
case .duckPlayerContingencyLearnMoreClicked: return "duckplayer_ios_contingency_learn-more-clicked"

// MARK: Enhanced statistics
case .usageSegments: return "m_retention_segments"

// MARK: Certificate warnings
case .certificateWarningDisplayed(let errorType):
return "m_certificate_warning_displayed_\(errorType)"
Expand All @@ -1600,6 +1615,14 @@ extension Pixel.Event {
case .pproFeedbackSubcategoryScreenShow: return "m_ppro_feedback_subcategory-screen_show"
case .pproFeedbackSubmitScreenShow: return "m_ppro_feedback_submit-screen_show"
case .pproFeedbackSubmitScreenFAQClick: return "m_ppro_feedback_submit-screen-faq_click"

// MARK: Duckplayer experiment
case .duckplayerExperimentCohortAssign: return "duckplayer_experiment_cohort_assign"
case .duckplayerExperimentSearch: return "duckplayer_experiment_search"
case .duckplayerExperimentDailySearch: return "duckplayer_experiment_daily_search"
case .duckplayerExperimentWeeklySearch: return "duckplayer_experiment_weekly_search"
case .duckplayerExperimentYoutubePageView: return "duckplayer_experiment_youtube_page_view"

}
}
}
Expand Down
32 changes: 29 additions & 3 deletions Core/StatisticsLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ public class StatisticsLoader {

private let statisticsStore: StatisticsStore
private let returnUserMeasurement: ReturnUserMeasurement
private let usageSegmentation: UsageSegmenting
private let parser = AtbParser()

init(statisticsStore: StatisticsStore = StatisticsUserDefaults(),
returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement()) {
returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(),
usageSegmentation: UsageSegmenting = UsageSegmentation()) {
self.statisticsStore = statisticsStore
self.returnUserMeasurement = returnUserMeasurement
self.usageSegmentation = usageSegmentation
}

public func load(completion: @escaping Completion = {}) {
Expand Down Expand Up @@ -88,7 +91,10 @@ public class StatisticsLoader {

public func refreshSearchRetentionAtb(completion: @escaping Completion = {}) {
guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeSearchAtbURL() else {
requestInstallStatistics(completion: completion)
requestInstallStatistics {
self.updateUsageSegmentationAfterInstall(activityType: .search)
completion()
}
return
}

Expand All @@ -104,6 +110,7 @@ public class StatisticsLoader {
if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) {
self.statisticsStore.searchRetentionAtb = atb.version
self.storeUpdateVersionIfPresent(atb)
self.updateUsageSegmentationWithAtb(atb, activityType: .search)
NotificationCenter.default.post(name: .searchDAU,
object: nil, userInfo: nil)
}
Expand All @@ -113,7 +120,10 @@ public class StatisticsLoader {

public func refreshAppRetentionAtb(completion: @escaping Completion = {}) {
guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeAppAtbURL() else {
requestInstallStatistics(completion: completion)
requestInstallStatistics {
self.updateUsageSegmentationAfterInstall(activityType: .appUse)
completion()
}
return
}

Expand All @@ -129,6 +139,7 @@ public class StatisticsLoader {
if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) {
self.statisticsStore.appRetentionAtb = atb.version
self.storeUpdateVersionIfPresent(atb)
self.updateUsageSegmentationWithAtb(atb, activityType: .appUse)
}
completion()
}
Expand All @@ -141,4 +152,19 @@ public class StatisticsLoader {
returnUserMeasurement.updateStoredATB(atb)
}
}

private func processUsageSegmentation(atb: Atb?, activityType: UsageActivityType) {
guard let installAtbValue = statisticsStore.atb else { return }
let installAtb = Atb(version: installAtbValue, updateVersion: nil)
let actualAtb = atb ?? installAtb
self.usageSegmentation.processATB(actualAtb, withInstallAtb: installAtb, andActivityType: activityType)
}

private func updateUsageSegmentationWithAtb(_ atb: Atb, activityType: UsageActivityType) {
processUsageSegmentation(atb: atb, activityType: activityType)
}

private func updateUsageSegmentationAfterInstall(activityType: UsageActivityType) {
processUsageSegmentation(atb: nil, activityType: activityType)
}
}
8 changes: 3 additions & 5 deletions Core/URLOpener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ import UIKit

public protocol URLOpener: AnyObject {
func canOpenURL(_ url: URL) -> Bool
func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?)
func open(_ url: URL)
}

public extension URLOpener {
func open(_ url: URL) {
extension UIApplication: URLOpener {
public func open(_ url: URL) {
open(url, options: [:], completionHandler: nil)
}
}

extension UIApplication: URLOpener {}
Loading

0 comments on commit a436229

Please sign in to comment.