From 708bab3e35cf813d92154f3394bfba187bf94cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Fri, 8 Nov 2024 09:45:04 +0100 Subject: [PATCH 1/7] Remove ATB from attribution pixel (#3550) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208686056380205/f Tech Design URL: CC: **Description**: Removes ATB cohort information from attribution pixel. Adds information about reinstall (returning user). **Steps to test this PR**: 1. On a fresh install check if the parameter is included. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/Pixel.swift | 1 + .../AdAttributionPixelReporter.swift | 18 ++++++++++-- .../AdAttributionPixelReporterTests.swift | 28 +++++++++++++++---- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Core/Pixel.swift b/Core/Pixel.swift index a7a6c7e3e2..b0796afe64 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -142,6 +142,7 @@ public struct PixelParameters { public static let adAttributionKeywordID = "keyword_id" public static let adAttributionAdID = "ad_id" public static let adAttributionToken = "attribution_token" + public static let adAttributionIsReinstall = "is_reinstall" // Autofill public static let countBucket = "count_bucket" diff --git a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift index 227ceeca0e..3119eedcf4 100644 --- a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift +++ b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift @@ -29,6 +29,7 @@ final actor AdAttributionPixelReporter { private let attributionFetcher: AdAttributionFetcher private let featureFlagger: FeatureFlagger private let privacyConfigurationManager: PrivacyConfigurationManaging + private let variantManager: VariantManager private let pixelFiring: PixelFiringAsync.Type private var isSendingAttribution: Bool = false @@ -50,12 +51,14 @@ final actor AdAttributionPixelReporter { attributionFetcher: AdAttributionFetcher = DefaultAdAttributionFetcher(), featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + variantManager: VariantManager = AppDependencyProvider.shared.variantManager, pixelFiring: PixelFiringAsync.Type = Pixel.self, inconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring = StorageInconsistencyMonitor()) { self.fetcherStorage = fetcherStorage self.attributionFetcher = attributionFetcher self.featureFlagger = featureFlagger self.privacyConfigurationManager = privacyConfigurationManager + self.variantManager = variantManager self.pixelFiring = pixelFiring self.inconsistencyMonitoring = inconsistencyMonitoring } @@ -85,12 +88,14 @@ final actor AdAttributionPixelReporter { if let (token, attributionData) = await self.attributionFetcher.fetch() { if attributionData.attribution { let settings = AdAttributionReporterSettings(privacyConfigurationManager.privacyConfig) - let parameters = self.pixelParametersForAttribution(attributionData, attributionToken: settings.includeToken ? token : nil) + let token = settings.includeToken ? token : nil + let isReinstall = variantManager.isIndicatingReturningUser + let parameters = self.pixelParametersForAttribution(attributionData, isReinstall: isReinstall, attributionToken: token) do { try await pixelFiring.fire( pixel: .appleAdAttribution, withAdditionalParameters: parameters, - includedParameters: [.appVersion, .atb] + includedParameters: [.appVersion] ) } catch { return false @@ -127,7 +132,7 @@ final actor AdAttributionPixelReporter { } } - private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse, attributionToken: String?) -> [String: String] { + private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse, isReinstall: Bool, attributionToken: String?) -> [String: String] { var params: [String: String] = [:] params[PixelParameters.adAttributionAdGroupID] = attribution.adGroupId.map(String.init) @@ -139,6 +144,7 @@ final actor AdAttributionPixelReporter { params[PixelParameters.adAttributionKeywordID] = attribution.keywordId.map(String.init) params[PixelParameters.adAttributionAdID] = attribution.adId.map(String.init) params[PixelParameters.adAttributionToken] = attributionToken + params[PixelParameters.adAttributionIsReinstall] = isReinstall ? "1" : "0" return params } @@ -161,3 +167,9 @@ private struct AdAttributionReporterSettings { static let includeToken = "includeToken" } } + +private extension VariantManager { + var isIndicatingReturningUser: Bool { + currentVariant?.name == VariantIOS.returningUser.name + } +} diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift index 4e403893a5..ea0a46d55a 100644 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift @@ -28,6 +28,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { private var fetcherStorage: AdAttributionReporterStorageMock! private var featureFlagger: MockFeatureFlagger! private var privacyConfigurationManager: PrivacyConfigurationManagerMock! + private var variantManager: MockVariantManager! private let fileMarker = BoolFileMarker(name: .init(rawValue: "ad-attribution-successful"))! @@ -36,6 +37,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { fetcherStorage = AdAttributionReporterStorageMock() featureFlagger = MockFeatureFlagger() privacyConfigurationManager = PrivacyConfigurationManagerMock() + variantManager = MockVariantManager() featureFlagger.enabledFeatureFlags.append(.adAttributionReporting) fileMarker.unmark() @@ -109,6 +111,19 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertEqual(pixelAttributes["keyword_id"], "4") XCTAssertEqual(pixelAttributes["ad_id"], "5") XCTAssertEqual(pixelAttributes["attribution_token"], "example") + XCTAssertEqual(pixelAttributes["is_reinstall"], "0") + } + + func testReinstallTrueWhenReturningUserVariantPresent() async throws { + let sut = createSUT(with: .returningUser) + attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + (privacyConfigurationManager.privacyConfig as? PrivacyConfigurationMock)?.settings[.adAttributionReporting] = ["includeToken": true] + + await sut.reportAttributionIfNeeded() + + let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) + + XCTAssertEqual(pixelAttributes["is_reinstall"], "1") } func testPixelAdditionalParameters() async throws { @@ -119,7 +134,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastIncludedParams) - XCTAssertEqual(pixelAttributes, [.appVersion, .atb]) + XCTAssertEqual(pixelAttributes, [.appVersion]) } func testPixelAttributes_WhenPartialAttributionData() async throws { @@ -220,17 +235,18 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertNotNil(pixelAttributes["attribution_token"]) } - private func createSUT() -> AdAttributionPixelReporter { + private func createSUT(with variant: VariantIOS? = nil) -> AdAttributionPixelReporter { AdAttributionPixelReporter(fetcherStorage: fetcherStorage, attributionFetcher: attributionFetcher, featureFlagger: featureFlagger, privacyConfigurationManager: privacyConfigurationManager, + variantManager: MockVariantManager(isSupportedReturns: false, currentVariant: variant), pixelFiring: PixelFiringMock.self, inconsistencyMonitoring: MockAdAttributionReporterInconsistencyMonitoring()) } } -class AdAttributionReporterStorageMock: AdAttributionReporterStorage { +private class AdAttributionReporterStorageMock: AdAttributionReporterStorage { func markAttributionReportSuccessful() async { wasAttributionReportSuccessful = true } @@ -238,7 +254,7 @@ class AdAttributionReporterStorageMock: AdAttributionReporterStorage { private(set) var wasAttributionReportSuccessful: Bool = false } -class AdAttributionFetcherMock: AdAttributionFetcher { +private class AdAttributionFetcherMock: AdAttributionFetcher { var wasFetchCalled: Bool = false var fetchResponse: (String, AdServicesAttributionResponse)? @@ -248,13 +264,13 @@ class AdAttributionFetcherMock: AdAttributionFetcher { } } -struct MockAdAttributionReporterInconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring { +private struct MockAdAttributionReporterInconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring { func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) { } } -extension AdServicesAttributionResponse { +private extension AdServicesAttributionResponse { init(attribution: Bool) { self.init( attribution: attribution, From 0f5996e84af68dfa721f18d7365485698eae046a Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Sat, 9 Nov 2024 00:36:40 +0100 Subject: [PATCH 2/7] point to BSK branch (#3559) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208592742896624/f **Description**: Update BSK Dependency --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f77ca62941..6f52dde323 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10986,7 +10986,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 207.0.0; + version = 207.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 db2341dd12..71287414b2 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" : "6319be3a8a52024c62cec4320e94536b51f427ee", - "version" : "207.0.0" + "revision" : "26cc3c597990db8a0f8aa4be743b25ce65076c95", + "version" : "207.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "6cab7bdb584653a5dc007cc1ae827ec41c5a91bc", - "version" : "6.29.0" + "revision" : "1733ee59f06f6e725a98cf6cd8322159f59d664b", + "version" : "6.31.0" } }, { From 1ae8ae8e27a7e48b5f5e7b6ae88d16adb1872634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Sat, 9 Nov 2024 01:49:43 +0100 Subject: [PATCH 3/7] Refresh toast updates (#3552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1207889813347128/f **Description**: We’re introducing a behavioral toast on macOS, which required moving logic from the iOS client into BSK. Some of this logic has beenupdated—specifically, we now only support an event for 3 consecutive refreshes, whereas previously, we had multiple events. --- Core/PixelEvent.swift | 14 +- Core/UserDefaultsPropertyWrapper.swift | 4 +- DuckDuckGo.xcodeproj/project.pbxproj | 66 ++++----- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/AppDependencyProvider.swift | 6 +- DuckDuckGo/AppPageRefreshMonitor.swift | 30 ++++ DuckDuckGo/BrokenSitePromptLimiter.swift | 106 -------------- DuckDuckGo/BrokenSitePromptLimiterStore.swift | 32 +++++ DuckDuckGo/BrokenSitePromptView.swift | 2 +- DuckDuckGo/MainViewController.swift | 29 ++-- DuckDuckGo/PageRefreshStore.swift | 29 ++++ DuckDuckGo/TabViewController.swift | 8 +- DuckDuckGo/UserBehaviorMonitor.swift | 132 ------------------ DuckDuckGo/UserText.swift | 2 +- DuckDuckGo/bg.lproj/Localizable.strings | 6 +- DuckDuckGo/cs.lproj/Localizable.strings | 6 +- DuckDuckGo/da.lproj/Localizable.strings | 6 +- DuckDuckGo/de.lproj/Localizable.strings | 6 +- DuckDuckGo/el.lproj/Localizable.strings | 6 +- DuckDuckGo/en.lproj/Localizable.strings | 6 +- DuckDuckGo/es.lproj/Localizable.strings | 6 +- DuckDuckGo/et.lproj/Localizable.strings | 6 +- DuckDuckGo/fi.lproj/Localizable.strings | 6 +- DuckDuckGo/fr.lproj/Localizable.strings | 6 +- DuckDuckGo/hr.lproj/Localizable.strings | 6 +- DuckDuckGo/hu.lproj/Localizable.strings | 6 +- DuckDuckGo/it.lproj/Localizable.strings | 6 +- DuckDuckGo/lt.lproj/Localizable.strings | 6 +- DuckDuckGo/lv.lproj/Localizable.strings | 6 +- DuckDuckGo/nb.lproj/Localizable.strings | 6 +- DuckDuckGo/nl.lproj/Localizable.strings | 6 +- DuckDuckGo/pl.lproj/Localizable.strings | 6 +- DuckDuckGo/pt.lproj/Localizable.strings | 6 +- DuckDuckGo/ro.lproj/Localizable.strings | 6 +- DuckDuckGo/ru.lproj/Localizable.strings | 6 +- DuckDuckGo/sk.lproj/Localizable.strings | 6 +- DuckDuckGo/sl.lproj/Localizable.strings | 6 +- DuckDuckGo/sv.lproj/Localizable.strings | 6 +- DuckDuckGo/tr.lproj/Localizable.strings | 6 +- .../BrokenSitePromptLimiterTests.swift | 82 ----------- .../UserBehaviorMonitorTests.swift | 124 ---------------- 41 files changed, 235 insertions(+), 585 deletions(-) create mode 100644 DuckDuckGo/AppPageRefreshMonitor.swift delete mode 100644 DuckDuckGo/BrokenSitePromptLimiter.swift create mode 100644 DuckDuckGo/BrokenSitePromptLimiterStore.swift create mode 100644 DuckDuckGo/PageRefreshStore.swift delete mode 100644 DuckDuckGo/UserBehaviorMonitor.swift delete mode 100644 DuckDuckGoTests/BrokenSitePromptLimiterTests.swift delete mode 100644 DuckDuckGoTests/UserBehaviorMonitorTests.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index a19da94024..35429325fc 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -671,8 +671,10 @@ extension Pixel { case toggleReportDoNotSend case toggleReportDismiss - case userBehaviorReloadTwiceWithin12Seconds - case userBehaviorReloadThreeTimesWithin20Seconds + case pageRefreshThreeTimesWithin20Seconds + + case siteNotWorkingShown + case siteNotWorkingWebsiteIsBroken // MARK: History case historyStoreLoadFailed @@ -1494,9 +1496,11 @@ extension Pixel.Event { // MARK: - Apple Ad Attribution case .appleAdAttribution: return "m_apple-ad-attribution" - // MARK: - User behavior - case .userBehaviorReloadTwiceWithin12Seconds: return "m_reload-twice-within-12-seconds" - case .userBehaviorReloadThreeTimesWithin20Seconds: return "m_reload-three-times-within-20-seconds" + // MARK: - Page refresh toasts + case .pageRefreshThreeTimesWithin20Seconds: return "m_reload-three-times-within-20-seconds" + + case .siteNotWorkingShown: return "m_site-not-working_shown" + case .siteNotWorkingWebsiteIsBroken: return "m_site-not-working_website-is-broken" // MARK: - History debug case .historyStoreLoadFailed: return "m_debug_history-store-load-failed" diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 1231840c9b..eae3ee9842 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -143,9 +143,7 @@ public struct UserDefaultsWrapper { case appleAdAttributionReportCompleted = "com.duckduckgo.ios.appleAdAttributionReport.completed" - case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp" - case didDoubleRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didDoubleRefreshTimestamp" - case didRefreshCounter = "com.duckduckgo.ios.userBehavior.didRefreshCounter" + case refreshTimestamps = "com.duckduckgo.ios.pageRefreshMonitor.refreshTimestamps" case lastBrokenSiteToastShownDate = "com.duckduckgo.ios.userBehavior.lastBrokenSiteToastShownDate" case toastDismissStreakCounter = "com.duckduckgo.ios.userBehavior.toastDismissStreakCounter" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6f52dde323..ea3eb46242 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -17,8 +17,7 @@ 026DABA428242BC80089E0B5 /* MockUserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026DABA328242BC80089E0B5 /* MockUserAgent.swift */; }; 0283A1FC2C6E3D8100508FBD /* BrokenSitePromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283A1FA2C6E3D8100508FBD /* BrokenSitePromptView.swift */; }; 0283A1FE2C6E3E1B00508FBD /* BrokenSitePromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283A1FB2C6E3D8100508FBD /* BrokenSitePromptViewModel.swift */; }; - 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283A2002C6E46E300508FBD /* BrokenSitePromptLimiter.swift */; }; - 0283A2042C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283A2032C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift */; }; + 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283A2002C6E46E300508FBD /* BrokenSitePromptLimiterStore.swift */; }; 02BA15B126A89ECA00472DD7 /* ios-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 02BA15B026A89ECA00472DD7 /* ios-config.json */; }; 02CA904924F6BFE700D41DDF /* navigatorsharepatch.js in Resources */ = {isa = PBXBuildFile; fileRef = 02CA904824F6BFE700D41DDF /* navigatorsharepatch.js */; }; 02CA904B24F6C11A00D41DDF /* NavigatorSharePatchUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA904A24F6C11A00D41DDF /* NavigatorSharePatchUserScript.swift */; }; @@ -940,12 +939,12 @@ CB2A7EEF283D185100885F67 /* RulesCompilationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EEE283D185100885F67 /* RulesCompilationMonitor.swift */; }; CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF028410DF700885F67 /* PixelEvent.swift */; }; CB2A7EF4285383B300885F67 /* AppLastCompiledRulesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */; }; - CB48D3332B90CE9F00631D8B /* UserBehaviorMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */; }; - CB48D3372B90DF2000631D8B /* UserBehaviorMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */; }; + CB48D3332B90CE9F00631D8B /* PageRefreshStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */; }; CB4FA44E2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */; }; CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */; }; CB5516D1286500290079B175 /* ContentBlockingRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA904C24FD2DB000D41DDF /* ContentBlockingRulesTests.swift */; }; CB5516D2286500290079B175 /* AtbServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F21DBD21121147002631A6 /* AtbServerTests.swift */; }; + CB6CC7E42CD2529000320907 /* BrokenSitePrompt in Frameworks */ = {isa = PBXBuildFile; productRef = CB6CC7E32CD2529000320907 /* BrokenSitePrompt */; }; CB6D8E982C80A9B100D0E772 /* SpecialErrorPages in Frameworks */ = {isa = PBXBuildFile; productRef = CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */; }; CB825C922C071B1400BCC586 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB825C912C071B1400BCC586 /* AlertView.swift */; }; CB825C962C071C9300BCC586 /* AlertViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB825C952C071C9300BCC586 /* AlertViewPresenter.swift */; }; @@ -967,6 +966,8 @@ CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */; }; CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */; }; CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE029A6741300832877 /* MockBundle.swift */; }; + CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */; }; + CBECDB7A2CD981CE005B8B87 /* AppPageRefreshMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB792CD981C6005B8B87 /* AppPageRefreshMonitor.swift */; }; CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */; }; CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */; }; D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60170BB2BA32DD6001911B5 /* Subscription.swift */; }; @@ -1335,8 +1336,7 @@ 026DABA328242BC80089E0B5 /* MockUserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserAgent.swift; sourceTree = ""; }; 0283A1FA2C6E3D8100508FBD /* BrokenSitePromptView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptView.swift; sourceTree = ""; }; 0283A1FB2C6E3D8100508FBD /* BrokenSitePromptViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptViewModel.swift; sourceTree = ""; }; - 0283A2002C6E46E300508FBD /* BrokenSitePromptLimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptLimiter.swift; sourceTree = ""; }; - 0283A2032C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptLimiterTests.swift; sourceTree = ""; }; + 0283A2002C6E46E300508FBD /* BrokenSitePromptLimiterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptLimiterStore.swift; sourceTree = ""; }; 02BA15B026A89ECA00472DD7 /* ios-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "ios-config.json"; sourceTree = ""; }; 02C4BC3127C3F9B600C40026 /* AppPrivacyConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPrivacyConfigurationTests.swift; sourceTree = ""; }; 02CA904824F6BFE700D41DDF /* navigatorsharepatch.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = navigatorsharepatch.js; sourceTree = ""; }; @@ -2743,8 +2743,7 @@ CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLastCompiledRulesStore.swift; sourceTree = ""; }; CB2C47822AF6D55800AEDCD9 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; CB4448752AF6D51D001F93F7 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = ""; }; - CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitor.swift; sourceTree = ""; }; - CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitorTests.swift; sourceTree = ""; }; + CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageRefreshStore.swift; sourceTree = ""; }; CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScript.swift; sourceTree = ""; }; CB5038622AF6D563007FD69F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; CB6ABD002AF6D52B004A8224 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2775,6 +2774,7 @@ CBDD5DE029A6741300832877 /* MockBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBundle.swift; sourceTree = ""; }; CBE099292AF6D54D000EFC47 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/InfoPlist.strings; sourceTree = ""; }; CBECB27B2AF6D58D006960FA /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + CBECDB792CD981C6005B8B87 /* AppPageRefreshMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPageRefreshMonitor.swift; sourceTree = ""; }; CBEF49902AF6D50600BFBD7D /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalAlerts.swift; sourceTree = ""; }; CBF0FA762AF6D4D800FB1C5B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -3148,6 +3148,7 @@ 98A50962294B48A400D10880 /* Bookmarks in Frameworks */, 1E60989B290009C700A508F9 /* Common in Frameworks */, 1E60989D290011E600A508F9 /* ContentBlocking in Frameworks */, + CB6CC7E42CD2529000320907 /* BrokenSitePrompt in Frameworks */, F486D33425069BBB002D07D7 /* Kingfisher in Frameworks */, EE8E568A2A56BCE400F11DCA /* NetworkProtection in Frameworks */, D6BC8ACB2C5AA3860025375B /* DuckPlayer in Frameworks */, @@ -3160,6 +3161,7 @@ 851481882A600EFC00ABC65F /* RemoteMessaging in Frameworks */, 37DF000C29F9CA80002B7D3E /* SyncDataProviders in Frameworks */, 1E6098A1290011E600A508F9 /* UserScript in Frameworks */, + CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */, D61CDA162B7CF77300A0FBB9 /* Subscription in Frameworks */, 858D009D2B9799FC004E5B4C /* History in Frameworks */, C14882ED27F211A000D59F0C /* SwiftSoup in Frameworks */, @@ -3198,15 +3200,7 @@ children = ( 0283A1FA2C6E3D8100508FBD /* BrokenSitePromptView.swift */, 0283A1FB2C6E3D8100508FBD /* BrokenSitePromptViewModel.swift */, - 0283A2002C6E46E300508FBD /* BrokenSitePromptLimiter.swift */, - ); - name = BrokenSitePrompt; - sourceTree = ""; - }; - 0283A2022C6E56F400508FBD /* BrokenSitePrompt */ = { - isa = PBXGroup; - children = ( - 0283A2032C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift */, + 0283A2002C6E46E300508FBD /* BrokenSitePromptLimiterStore.swift */, ); name = BrokenSitePrompt; sourceTree = ""; @@ -4256,7 +4250,7 @@ 98F3A1D6217B36EE0011A0D4 /* Themes */, 7BF78E002CA2CC100026A1FC /* TipKit */, F11CEF581EBB66C80088E4D7 /* Tutorials */, - CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */, + CB48D32F2B90CE8500631D8B /* PageRefreshMonitor */, F1D796ED1E7AE4090019D451 /* UserInterface */, 84E341E31E2FC0E400BDBA6F /* UserInterfaceResources */, 3151F0E827357F8F00226F58 /* VoiceSearch */, @@ -5289,20 +5283,13 @@ path = Configuration; sourceTree = ""; }; - CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */ = { + CB48D32F2B90CE8500631D8B /* PageRefreshMonitor */ = { isa = PBXGroup; children = ( - CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */, + CBECDB792CD981C6005B8B87 /* AppPageRefreshMonitor.swift */, + CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */, ); - name = UserBehaviorMonitor; - sourceTree = ""; - }; - CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */ = { - isa = PBXGroup; - children = ( - CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */, - ); - name = UserBehaviorMonitor; + name = PageRefreshMonitor; sourceTree = ""; }; CBAA195627BFDD9800A4BD49 /* SmarterEncryption */ = { @@ -5765,7 +5752,6 @@ 981FED7222045FFA008488D7 /* AutoClear */, 1E1D8B5B2994FF7800C96994 /* Autoconsent */, F40F843228C92B1C0081AE75 /* Autofill */, - 0283A2022C6E56F400508FBD /* BrokenSitePrompt */, 98559FD0267099F400A83094 /* ContentBlocker */, 31C138A127A334F600FFD4B2 /* Downloads */, D62EC3B72C24695800FC9D04 /* DuckPlayer */, @@ -5781,7 +5767,6 @@ F13B4BF71F18C9E800814661 /* Tabs */, 98EA2C3A218B9A880023E1DC /* Themes */, F12790DD1EBBDDF3001D3AEC /* Tutorials */, - CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */, F194FAF91F14E605009B4DF8 /* UserInterface */, 317045BE2858C69A0016ED1F /* Utils */, 4B6484F927FFCF520050A7A1 /* Waitlist */, @@ -6792,6 +6777,8 @@ 851F74252B9A1BFD00747C42 /* Suggestions */, D6BC8ACA2C5AA3860025375B /* DuckPlayer */, CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */, + CB6CC7E32CD2529000320907 /* BrokenSitePrompt */, + CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */, ); productName = Core; productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */; @@ -7626,6 +7613,7 @@ CB4FA44E2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift in Sources */, EE0153ED2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift in Sources */, EEF0F8CC2ABC832300630031 /* NetworkProtectionDebugFeatures.swift in Sources */, + CBECDB7A2CD981CE005B8B87 /* AppPageRefreshMonitor.swift in Sources */, B60DFF072872B64B0061E7C2 /* JSAlertController.swift in Sources */, 981FED6E22025151008488D7 /* BlankSnapshotViewController.swift in Sources */, D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */, @@ -7775,7 +7763,7 @@ 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, 310C4B47281B60E300BA79A9 /* AutofillLoginDetailsViewModel.swift in Sources */, 85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */, - CB48D3332B90CE9F00631D8B /* UserBehaviorMonitor.swift in Sources */, + CB48D3332B90CE9F00631D8B /* PageRefreshStore.swift in Sources */, 1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */, 1E4DCF4E27B6A69600961E25 /* DownloadsListHostingController.swift in Sources */, 850F93DB2B594AB800823EEA /* ZippedPassKitPreviewHelper.swift in Sources */, @@ -7825,7 +7813,7 @@ C13F3F6A2B7F883A0083BE40 /* AuthConfirmationPromptViewController.swift in Sources */, 851624C72B96389D002D5CD7 /* HistoryDebugViewController.swift in Sources */, 8540BBA22440857A00017FE4 /* PreserveLoginsWorker.swift in Sources */, - 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiter.swift in Sources */, + 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiterStore.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, 851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */, 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, @@ -8045,7 +8033,6 @@ 6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */, 987130C5294AAB9F00AB05E0 /* BookmarkEditorViewModelTests.swift in Sources */, BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, - 0283A2042C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift in Sources */, 9F8E0F332CCA642D001EA7C5 /* VideoPlayerViewModelTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, @@ -8065,7 +8052,6 @@ 9F5E5AB22C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift in Sources */, 9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */, F13B4BFB1F18E3D900814661 /* TabsModelPersistenceExtensionTests.swift in Sources */, - CB48D3372B90DF2000631D8B /* UserBehaviorMonitorTests.swift in Sources */, 8528AE7E212EF5FF00D0BD74 /* AppRatingPromptTests.swift in Sources */, 981FED692201FE69008488D7 /* AutoClearSettingsScreenTests.swift in Sources */, 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */, @@ -10986,7 +10972,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 207.1.0; + version = 208.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -11211,6 +11197,10 @@ package = C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; + CB6CC7E32CD2529000320907 /* BrokenSitePrompt */ = { + isa = XCSwiftPackageProductDependency; + productName = BrokenSitePrompt; + }; CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -11226,6 +11216,10 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Configuration; }; + CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */ = { + isa = XCSwiftPackageProductDependency; + productName = PageRefreshMonitor; + }; D61CDA152B7CF77300A0FBB9 /* Subscription */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 71287414b2..49d5d017ea 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" : "26cc3c597990db8a0f8aa4be743b25ce65076c95", - "version" : "207.1.0" + "revision" : "17154907fe86c75942331ed6d037694c666ddd95", + "version" : "208.0.0" } }, { diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 4997f73e56..35d7bf45d0 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -26,6 +26,7 @@ import Subscription import Common import NetworkProtection import RemoteMessaging +import PageRefreshMonitor protocol DependencyProvider { @@ -39,7 +40,7 @@ protocol DependencyProvider { var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager { get } var configurationManager: ConfigurationManager { get } var configurationStore: ConfigurationStore { get } - var userBehaviorMonitor: UserBehaviorMonitor { get } + var pageRefreshMonitor: PageRefreshMonitor { get } var subscriptionManager: SubscriptionManager { get } var accountManager: AccountManager { get } var vpnFeatureVisibility: DefaultNetworkProtectionVisibility { get } @@ -72,7 +73,8 @@ final class AppDependencyProvider: DependencyProvider { let configurationManager: ConfigurationManager let configurationStore = ConfigurationStore() - let userBehaviorMonitor = UserBehaviorMonitor() + let pageRefreshMonitor = PageRefreshMonitor(onDidDetectRefreshPattern: PageRefreshMonitor.onDidDetectRefreshPattern, + store: PageRefreshStore()) // Subscription let subscriptionManager: SubscriptionManager diff --git a/DuckDuckGo/AppPageRefreshMonitor.swift b/DuckDuckGo/AppPageRefreshMonitor.swift new file mode 100644 index 0000000000..7152b57f75 --- /dev/null +++ b/DuckDuckGo/AppPageRefreshMonitor.swift @@ -0,0 +1,30 @@ +// +// AppPageRefreshMonitor.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 Common +import PageRefreshMonitor + +extension PageRefreshMonitor { + + static let onDidDetectRefreshPattern: () -> Void = { + Pixel.fire(pixel: .pageRefreshThreeTimesWithin20Seconds) + } + +} diff --git a/DuckDuckGo/BrokenSitePromptLimiter.swift b/DuckDuckGo/BrokenSitePromptLimiter.swift deleted file mode 100644 index 69b962ab13..0000000000 --- a/DuckDuckGo/BrokenSitePromptLimiter.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// BrokenSitePromptLimiter.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 -import BrowserServicesKit - -protocol BrokenSitePromptLimiterStoring { - var lastToastShownDate: Date { get set } - var toastDismissStreakCounter: Int { get set } -} - -final class BrokenSitePromptLimiterStore: BrokenSitePromptLimiterStoring { - @UserDefaultsWrapper(key: .lastBrokenSiteToastShownDate, defaultValue: .distantPast) - var lastToastShownDate: Date - - @UserDefaultsWrapper(key: .toastDismissStreakCounter, defaultValue: 0) - var toastDismissStreakCounter: Int -} - -final class BrokenSitePromptLimiter { - - struct BrokenSitePromptLimiterSettings: Codable { - let maxDismissStreak: Int - let dismissStreakResetDays: Int - let coolDownDays: Int - } - - private var lastToastShownDate: Date { - get { store.lastToastShownDate } - set { store.lastToastShownDate = newValue } - } - - private var toastDismissStreakCounter: Int { - get { store.toastDismissStreakCounter } - set { store.toastDismissStreakCounter = newValue } - } - - private var privacyConfigManager: PrivacyConfigurationManaging - private var store: BrokenSitePromptLimiterStoring - - init(privacyConfigManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, - store: BrokenSitePromptLimiterStoring = BrokenSitePromptLimiterStore()) { - self.privacyConfigManager = privacyConfigManager - self.store = store - } - - private func getSettingsFromConfig() -> BrokenSitePromptLimiterSettings { - let settings = privacyConfigManager.privacyConfig.settings(for: .brokenSitePrompt) - - // Get settings from config or fallback to standard defaults - return BrokenSitePromptLimiterSettings( - maxDismissStreak: settings["maxDismissStreak"] as? Int ?? 3, - dismissStreakResetDays: settings["dismissStreakResetDays"] as? Int ?? 30, - coolDownDays: settings["coolDownDays"] as? Int ?? 7 - ) - } - - /// If it has been `dismissStreakResetDays` or more since the last time we showed the prompt, reset the dismiss counter to 0 - private func resetDismissStreakIfNeeded(dismissStreakResetDays: Int) { - if !lastToastShownDate.isLessThan(daysAgo: dismissStreakResetDays) { - toastDismissStreakCounter = 0 - } - } - - public func shouldShowToast() -> Bool { - guard privacyConfigManager.privacyConfig.isEnabled(featureKey: .brokenSitePrompt) else { return false } - - let settings = getSettingsFromConfig() - - resetDismissStreakIfNeeded(dismissStreakResetDays: settings.dismissStreakResetDays) - guard toastDismissStreakCounter < settings.maxDismissStreak else { return false } // Don't show the toast if the user dismissed it more than `maxDismissStreak` times in a row - guard !lastToastShownDate.isLessThan(daysAgo: settings.coolDownDays) else { return false } // Only show the toast once per `coolDownDays` days - - return true - } - - public func didShowToast() { - lastToastShownDate = Date() - } - - public func didDismissToast() { - toastDismissStreakCounter += 1 - } - - public func didOpenReport() { - toastDismissStreakCounter = 0 - } - -} diff --git a/DuckDuckGo/BrokenSitePromptLimiterStore.swift b/DuckDuckGo/BrokenSitePromptLimiterStore.swift new file mode 100644 index 0000000000..9236ee3f0e --- /dev/null +++ b/DuckDuckGo/BrokenSitePromptLimiterStore.swift @@ -0,0 +1,32 @@ +// +// BrokenSitePromptLimiterStore.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 BrokenSitePrompt +import Core + +final class BrokenSitePromptLimiterStore: BrokenSitePromptLimiterStoring { + + @UserDefaultsWrapper(key: .lastBrokenSiteToastShownDate, defaultValue: .distantPast) + var lastToastShownDate: Date + + @UserDefaultsWrapper(key: .toastDismissStreakCounter, defaultValue: 0) + var toastDismissStreakCounter: Int + +} diff --git a/DuckDuckGo/BrokenSitePromptView.swift b/DuckDuckGo/BrokenSitePromptView.swift index b04b06a778..2f89e67dce 100644 --- a/DuckDuckGo/BrokenSitePromptView.swift +++ b/DuckDuckGo/BrokenSitePromptView.swift @@ -39,7 +39,7 @@ struct BrokenSitePromptView: View { Button(UserText.siteNotWorkingDismiss, action: viewModel.onDidDismiss) .buttonStyle(GhostButtonStyle()) .fixedSize() - Button(UserText.siteNotWorkingWebsiteIsBroken, action: viewModel.onDidSubmit) + Button(UserText.siteNotWorkingReportBrokenSite, action: viewModel.onDidSubmit) .buttonStyle(PrimaryButtonStyle(compact: true)) .fixedSize() } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 9726c00630..de806d9737 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -36,6 +36,8 @@ import SwiftUI import NetworkProtection import Onboarding import os.log +import PageRefreshMonitor +import BrokenSitePrompt class MainViewController: UIViewController { @@ -301,7 +303,7 @@ class MainViewController: UIViewController { findInPageView.delegate = self findInPageBottomLayoutConstraint.constant = 0 registerForKeyboardNotifications() - registerForUserBehaviorEvents() + registerForPageRefreshPatterns() registerForSyncFeatureFlagsUpdates() decorate() @@ -497,11 +499,11 @@ class MainViewController: UIViewController { keyboardShowing = false } - private func registerForUserBehaviorEvents() { + private func registerForPageRefreshPatterns() { NotificationCenter.default.addObserver( self, selector: #selector(attemptToShowBrokenSitePrompt(_:)), - name: .userBehaviorDidMatchBrokenSiteCriteria, + name: .pageRefreshMonitorDidDetectRefreshPattern, object: nil) } @@ -1095,8 +1097,7 @@ class MainViewController: UIViewController { } private func hideNotificationBarIfBrokenSitePromptShown(afterRefresh: Bool = false) { - guard brokenSitePromptViewHostingController != nil, - let event = brokenSitePromptEvent?.rawValue else { return } + guard brokenSitePromptViewHostingController != nil else { return } brokenSitePromptViewHostingController = nil hideNotification() } @@ -1341,12 +1342,11 @@ class MainViewController: UIViewController { } private var brokenSitePromptViewHostingController: UIHostingController? - private var brokenSitePromptEvent: UserBehaviorEvent? - lazy private var brokenSitePromptLimiter = BrokenSitePromptLimiter() + lazy private var brokenSitePromptLimiter = BrokenSitePromptLimiter(privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + store: BrokenSitePromptLimiterStore()) @objc func attemptToShowBrokenSitePrompt(_ notification: Notification) { guard brokenSitePromptLimiter.shouldShowToast(), - let event = notification.userInfo?[UserBehaviorEvent.Key.event] as? UserBehaviorEvent, let url = currentTab?.url, !url.isDuckDuckGo, notificationView == nil, !isPad, @@ -1356,18 +1356,18 @@ class MainViewController: UIViewController { // We're using async to ensure the view dismissal happens on the first runloop after a refresh. This prevents the scenario where the view briefly appears and then immediately disappears after a refresh. brokenSitePromptLimiter.didShowToast() DispatchQueue.main.async { - self.showBrokenSitePrompt(after: event) + self.showBrokenSitePrompt() } } - private func showBrokenSitePrompt(after event: UserBehaviorEvent) { - let host = makeBrokenSitePromptViewHostingController(event: event) + private func showBrokenSitePrompt() { + let host = makeBrokenSitePromptViewHostingController() brokenSitePromptViewHostingController = host - brokenSitePromptEvent = event + Pixel.fire(pixel: .siteNotWorkingShown) showNotification(with: host.view) } - private func makeBrokenSitePromptViewHostingController(event: UserBehaviorEvent) -> UIHostingController { + private func makeBrokenSitePromptViewHostingController() -> UIHostingController { let viewModel = BrokenSitePromptViewModel(onDidDismiss: { [weak self] in Task { @MainActor in self?.hideNotification() @@ -1376,10 +1376,11 @@ class MainViewController: UIViewController { } }, onDidSubmit: { [weak self] in Task { @MainActor in - self?.segueToReportBrokenSite(entryPoint: .prompt(event.rawValue)) + self?.segueToReportBrokenSite(entryPoint: .prompt) self?.hideNotification() self?.brokenSitePromptLimiter.didOpenReport() self?.brokenSitePromptViewHostingController = nil + Pixel.fire(pixel: .siteNotWorkingWebsiteIsBroken) } }) return UIHostingController(rootView: BrokenSitePromptView(viewModel: viewModel), ignoreSafeArea: true) diff --git a/DuckDuckGo/PageRefreshStore.swift b/DuckDuckGo/PageRefreshStore.swift new file mode 100644 index 0000000000..2f0a0baad9 --- /dev/null +++ b/DuckDuckGo/PageRefreshStore.swift @@ -0,0 +1,29 @@ +// +// PageRefreshStore.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 +import PageRefreshMonitor + +final class PageRefreshStore: PageRefreshStoring { + + @UserDefaultsWrapper(key: .refreshTimestamps, defaultValue: []) + var refreshTimestamps: [Date] + +} diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 94a89dec38..6be661152c 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -621,7 +621,9 @@ class TabViewController: UIViewController { reload() delegate?.tabDidRequestRefresh(tab: self) Pixel.fire(pixel: .pullToRefresh) - AppDependencyProvider.shared.userBehaviorMonitor.handleRefreshAction() + if let url = webView.url { + AppDependencyProvider.shared.pageRefreshMonitor.register(for: url) + } }, for: .valueChanged) refreshControl.backgroundColor = .systemBackground @@ -2473,7 +2475,9 @@ extension TabViewController: UIGestureRecognizerDelegate { } refreshCountSinceLoad += 1 - AppDependencyProvider.shared.userBehaviorMonitor.handleRefreshAction() + if let url { + AppDependencyProvider.shared.pageRefreshMonitor.register(for: url) + } } } diff --git a/DuckDuckGo/UserBehaviorMonitor.swift b/DuckDuckGo/UserBehaviorMonitor.swift deleted file mode 100644 index 4d905cbdd5..0000000000 --- a/DuckDuckGo/UserBehaviorMonitor.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// UserBehaviorMonitor.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 Common -import Core - -public extension Notification.Name { - - static let userBehaviorDidMatchBrokenSiteCriteria = Notification.Name("com.duckduckgo.app.userBehaviorDidMatchBrokenSiteCriteria") - -} - -public enum UserBehaviorEvent: String { - - public enum Key { - - static let event = "com.duckduckgo.com.userBehaviorEvent.key" - - } - - case reloadTwiceWithin12Seconds = "reload-twice-within-12-seconds" - case reloadThreeTimesWithin20Seconds = "reload-three-times-within-20-seconds" - -} - -protocol UserBehaviorStoring { - - var didRefreshTimestamp: Date? { get set } - var didDoubleRefreshTimestamp: Date? { get set } - var didRefreshCounter: Int { get set } - -} - -final class UserBehaviorStore: UserBehaviorStoring { - - @UserDefaultsWrapper(key: .didRefreshTimestamp, defaultValue: .distantPast) - var didRefreshTimestamp: Date? - - @UserDefaultsWrapper(key: .didDoubleRefreshTimestamp, defaultValue: .distantPast) - var didDoubleRefreshTimestamp: Date? - - @UserDefaultsWrapper(key: .didRefreshCounter, defaultValue: 0) - var didRefreshCounter: Int - -} - -final class UserBehaviorMonitor { - - enum Action: Equatable { - - case refresh - - } - - private let eventMapping: EventMapping - private var store: UserBehaviorStoring - - init(eventMapping: EventMapping = AppUserBehaviorMonitor.eventMapping, - store: UserBehaviorStoring = UserBehaviorStore()) { - self.eventMapping = eventMapping - self.store = store - } - - var didRefreshTimestamp: Date? { - get { store.didRefreshTimestamp } - set { store.didRefreshTimestamp = newValue } - } - - var didDoubleRefreshTimestamp: Date? { - get { store.didDoubleRefreshTimestamp } - set { store.didDoubleRefreshTimestamp = newValue } - } - - var didRefreshCounter: Int { - get { store.didRefreshCounter } - set { store.didRefreshCounter = newValue } - } - - func handleRefreshAction(date: Date = Date()) { - fireEventIfActionOccurredRecently(within: 12.0, since: didRefreshTimestamp, eventToFire: .reloadTwiceWithin12Seconds) - didRefreshTimestamp = date - - if didRefreshCounter == 0 { - didDoubleRefreshTimestamp = date - } - didRefreshCounter += 1 - if didRefreshCounter > 2 { - fireEventIfActionOccurredRecently(within: 20.0, since: didDoubleRefreshTimestamp, eventToFire: .reloadThreeTimesWithin20Seconds) - didRefreshCounter = 0 - } - - func fireEventIfActionOccurredRecently(within interval: Double = 30.0, since timestamp: Date?, eventToFire: UserBehaviorEvent) { - if let timestamp = timestamp, date.timeIntervalSince(timestamp) < interval { - eventMapping.fire(eventToFire) - NotificationCenter.default.post(name: .userBehaviorDidMatchBrokenSiteCriteria, - object: self, - userInfo: [UserBehaviorEvent.Key.event: eventToFire]) - } - } - } - -} - -final class AppUserBehaviorMonitor { - - static let eventMapping = EventMapping { event, _, _, _ in - let domainEvent: Pixel.Event - switch event { - case .reloadTwiceWithin12Seconds: domainEvent = .userBehaviorReloadTwiceWithin12Seconds - case .reloadThreeTimesWithin20Seconds: domainEvent = .userBehaviorReloadThreeTimesWithin20Seconds - } - Pixel.fire(pixel: domainEvent) - } - -} diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index b32b0c035f..c526e40a7d 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1258,7 +1258,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let siteNotWorkingTitle = NSLocalizedString("site.not.working.title", value: "Site not working? Let DuckDuckGo know.", comment: "Prompt asking user to send report to us if we suspect site may be broken") public static let siteNotWorkingSubtitle = NSLocalizedString("site.not.working.subtitle", value: "This helps us improve the browser.", comment: "Prompt asking user to send report to us if we suspect site may be broken") public static let siteNotWorkingDismiss = NSLocalizedString("site.not.working.dismiss", value: "Dismiss", comment: "Dismiss button") - public static let siteNotWorkingWebsiteIsBroken = NSLocalizedString("site.not.working.website.is.broken", value: "Website Is Broken", comment: "Button that triggers flow to report broken site") + public static let siteNotWorkingReportBrokenSite = NSLocalizedString("site.not.working.report.broken.site", value: "Report Broken Site", comment: "Button that triggers flow to report broken site") public static let siteNotWorkingDescription = NSLocalizedString("site.not.working.description", value: "Select the option that best describes the problem you experienced.", comment: "Description on a report broken site page.") // Broken site report experiment diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index fe5c65f295..df72ca960f 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -2255,15 +2255,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Отказване"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Подаване на сигнал за повреден сайт"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Това ни помага да подобрим браузъра."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Сайтът не работи ли? Уведомете DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Уебсайтът е повреден"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Изпращане на доклад"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index 45df7910f4..86a9a53eb4 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Odmítnout"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Nahlásit nefunkční webové stránky"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Pomáhá nám to vylepšovat prohlížeč."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Stránka nefunguje? Dej DuckDuckGo vědět."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Web nefunguje"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Odešlete zprávu"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index 382ee89485..3e89b7a158 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Afvis"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Rapporter ødelagt websted"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Det hjælper os med at forbedre browseren."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Virker webstedet ikke? Fortæl DuckDuckGo det."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Hvilket websted er ødelagt?"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Indsend rapport"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 5344dede4c..5f893a3166 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Verwerfen"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Fehlerhafte Website melden"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Das hilft uns, den Browser zu verbessern."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Webseite funktioniert nicht? Sag DuckDuckGo Bescheid."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Website ist fehlerhaft"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Bericht senden"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 70c05970bd..5df4692edf 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Απόρριψη"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Αναφορά ιστότοπου που δεν λειτουργεί"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Αυτό μας βοηθά να βελτιώνουμε το πρόγραμμα περιήγησης."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Ο ιστότοπος δεν λειτουργεί; Ενημερώστε σχετικά το DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Ο ιστότοπος δεν λειτουργεί"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Υποβολή αναφοράς"; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 98f79b49e5..4ab56d6e1f 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2409,15 +2409,15 @@ But if you *do* want a peek under the hood, you can find more information about /* Dismiss button */ "site.not.working.dismiss" = "Dismiss"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Report Broken Site"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "This helps us improve the browser."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Site not working? Let DuckDuckGo know."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Website Is Broken"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Submit Report"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 0b7bd2e9e0..b7dff0c463 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Descartar"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Informar de sitio web dañado"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Esto nos ayuda a mejorar el navegador."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "¿El sitio no funciona? Házselo saber a DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "El sitio web no funciona"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Enviar informe"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index 38a5acaea0..b1376a0ab7 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Loobu"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Teata mittetoimivast saidist"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "See aitab meil brauserit täiustada."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Kas sait ei tööta? Anna DuckDuckGole teada."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Veebisait ei toimi"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Saada aruanne"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index d0c31a4164..c745173f5a 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Hylkää"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Ilmoita viallisesta sivustosta"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Tämä auttaa meitä parantamaan selainta."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Eikö sivusto toimi? Kerro siitä DuckDuckGolle."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Verkkosivusto on viallinen"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Lähetä raportti"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index f900fa4815..970ec2d762 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Ignorer"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Signaler un problème de site"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Cela nous aide à améliorer le navigateur."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Le site ne fonctionne pas ? Faites-le savoir à DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Le site Web pose problème"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Envoyer le rapport"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 269e54b844..1e91cfa4d0 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Odbaci"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Prijavi neispravno web-mjesto"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Ovo nam pomaže da poboljšamo preglednik."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Web-mjesto ne funkcionira? Javi DuckDuckGou."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Web-mjesto nije ispravno"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Pošalji izvješće"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 52618f6a75..6e011c1d33 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Elutasítás"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Hibás weboldal jelentése"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Ez segít minket a böngésző tökéletesítésében."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Nem működik a webhely? Jelezd a DuckDuckGo felé."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Nem működik a webhely"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Jelentés beküldése"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index e01a9d279f..aa975129cd 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Ignora"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Segnala sito danneggiato"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Questo ci aiuta a migliorare il browser."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Il sito non funziona? Comunicalo a DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Il sito web è danneggiato"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Invia segnalazione"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index c116362c61..aefc629f07 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Atmesti"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Pranešti apie sugadintą svetainę"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Tai padeda mums tobulinti naršyklę."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Svetainė neveikia? Praneškite „DuckDuckGo“."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Svetainė neveikia"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Pateikti ataskaitą"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 8f9c980cc1..53a83ff1b3 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Nerādīt"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Ziņot par bojātu vietni"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Tas mums palīdz uzlabot pārlūkprogrammu."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Vai vietne nedarbojas? Informē DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Vietne ir bojāta"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Iesniegt ziņojumu"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index cd0ded480c..5c75a162e9 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Avvis"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Rapporter nettstedfeil"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Dette hjelper oss med å forbedre nettleseren."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Fungerer ikke nettstedet? Gi beskjed til DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Nettstedet fungerer ikke"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Send inn rapport"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index 0ae46ad07e..49b2ffc582 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Negeren"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Defecte website melden"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Hiermee kunnen we de browser verbeteren."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Werkt de site niet? Laat het DuckDuckGo weten."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "De website werkt niet"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Rapport versturen"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 9888d7bfe0..a82a482b04 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Odrzuć"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Zgłoś uszkodzoną witrynę"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "To pomaga nam ulepszyć przeglądarkę."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Witryna nie działa? Poinformuj DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Witryna nie działa poprawnie"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Prześlij raport"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index 7fd8ef149b..fbef6e16e2 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Ignorar"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Denunciar site danificado"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Isto ajuda-nos a melhorar o navegador."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "O site não funciona? Informa o DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "O site não funciona"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Submeter relatório"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index ee562a26d3..a0d15fec4e 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Renunță"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Raportați site-ul defect"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Acest lucru ne ajută să îmbunătățim browserul."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Site-ul nu funcționează? Anunță DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Site-ul este nefuncțional"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Trimite raportul"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 5d041fb58f..35e1e8e6da 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Отклонить"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Сообщить о неработающем сайте"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Ваш ответ поможет нам улучшить работу браузера."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Сайт не работает? Сообщите в DuckDuckGo!"; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Проблема на сайте"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Отправить жалобу"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 43c3005757..e3ef7a8a83 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Odmietnuť"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Nahlásiť nefunkčnú stránku"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Toto nám pomáha vylepšiť prehliadač."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Stránka nefunguje? Oznámte to DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Webová stránka je nefunkčná"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Odoslať správu"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 088ce79181..08fd2dde74 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Opusti"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Prijavi nedelujočo stran"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "To nam pomaga izboljšati brskalnik."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Stran ne deluje? Sporočite DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Spletna stran je nedelujoča"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Pošlji poročilo"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index de228cd093..8b073384b5 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Avvisa"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Rapportera skadad webbplats"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Det hjälper oss att förbättra webbläsaren."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Fungerar inte webbplatsen? Informera DuckDuckGo."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Webbplatsen fungerar inte"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Skicka in rapport"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index 5b967dc65a..abaf73d410 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -2253,15 +2253,15 @@ /* Dismiss button */ "site.not.working.dismiss" = "Reddet"; +/* Button that triggers flow to report broken site */ +"site.not.working.report.broken.site" = "Hatalı Siteyi Bildir"; + /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.subtitle" = "Bu gibi bildirimler tarayıcıyı geliştirmemize yardımcı olur."; /* Prompt asking user to send report to us if we suspect site may be broken */ "site.not.working.title" = "Site çalışmıyor mu? DuckDuckGo'ya bildirin."; -/* Button that triggers flow to report broken site */ -"site.not.working.website.is.broken" = "Web Sitesi Bozuk"; - /* Report a Broken Site screen confirmation button */ "siteFeedback.buttonText" = "Rapor Gönder"; diff --git a/DuckDuckGoTests/BrokenSitePromptLimiterTests.swift b/DuckDuckGoTests/BrokenSitePromptLimiterTests.swift deleted file mode 100644 index c83836915c..0000000000 --- a/DuckDuckGoTests/BrokenSitePromptLimiterTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// BrokenSitePromptLimiterTests.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 -@testable import Core - -final class MockBrokenSitePromptLimiterStore: BrokenSitePromptLimiterStoring { - var lastToastShownDate: Date = .distantPast - var toastDismissStreakCounter: Int = 0 -} - -final class BrokenSitePromptLimiterTests: XCTestCase { - - let configManager = PrivacyConfigurationManagerMock() - var brokenSiteLimiter: BrokenSitePromptLimiter! - var mockStore: MockBrokenSitePromptLimiterStore! - - override func setUp() { - super.setUp() - - (configManager.privacyConfig as? PrivacyConfigurationMock)?.enabledFeaturesForVersions[.brokenSitePrompt] = [AppVersionProvider().appVersion() ?? ""] - - mockStore = MockBrokenSitePromptLimiterStore() - brokenSiteLimiter = BrokenSitePromptLimiter(privacyConfigManager: configManager, store: mockStore) - } - - func testShouldNotShowPromptIfConfigDisabled() throws { - (configManager.privacyConfig as? PrivacyConfigurationMock)?.enabledFeaturesForVersions[.brokenSitePrompt] = [] - XCTAssertFalse(brokenSiteLimiter.shouldShowToast(), "Toast should not show if disabled via config") - } - - func testShouldShowPromptOnFirstActivationThenLimit() throws { - XCTAssertTrue(brokenSiteLimiter.shouldShowToast(), "Toast should show on first activation") - brokenSiteLimiter.didShowToast() - XCTAssertFalse(brokenSiteLimiter.shouldShowToast(), "Subsequent call should not show toast due to limiting logic") - } - - func testShouldShowPromptAgainAfter7days() throws { - XCTAssertTrue(brokenSiteLimiter.shouldShowToast(), "Toast should show on first activation") - brokenSiteLimiter.didShowToast() - XCTAssertFalse(brokenSiteLimiter.shouldShowToast(), "Subsequent call should not show toast due to limiting logic") - mockStore.lastToastShownDate = Date().addingTimeInterval(-7 * 24 * 60 * 60 - 1) - XCTAssertTrue(brokenSiteLimiter.shouldShowToast(), "Toast should show again after 7 days") - } - - func testShouldNotShowPromptAfter3Dismissals() throws { - brokenSiteLimiter.didDismissToast() - brokenSiteLimiter.didDismissToast() - brokenSiteLimiter.didDismissToast() - // Set last date 7 days back so the toast shows but doesn't reset dismiss counter - mockStore.lastToastShownDate = Date().addingTimeInterval(-7 * 24 * 60 * 60 - 1) - XCTAssertFalse(brokenSiteLimiter.shouldShowToast(), "Toast should not show again after 3 dismissals") - } - - func testShouldResetDismissCounterAfter30Days() throws { - brokenSiteLimiter.didDismissToast() - brokenSiteLimiter.didDismissToast() - brokenSiteLimiter.didDismissToast() - XCTAssert(mockStore.toastDismissStreakCounter == 3, "Dismiss count should be equal to 3 after 3 dismiss calls") - XCTAssertTrue(brokenSiteLimiter.shouldShowToast(), "Toast should show after resetting counter") - XCTAssert(mockStore.toastDismissStreakCounter == 0, "Dismiss count should be reset to 0 after 30 days") - } - -} diff --git a/DuckDuckGoTests/UserBehaviorMonitorTests.swift b/DuckDuckGoTests/UserBehaviorMonitorTests.swift deleted file mode 100644 index 265a05484d..0000000000 --- a/DuckDuckGoTests/UserBehaviorMonitorTests.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// UserBehaviorMonitorTests.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 Common -@testable import DuckDuckGo - -final class MockUserBehaviorEventsMapping: EventMapping { - - init(captureEvent: @escaping (UserBehaviorEvent) -> Void) { - super.init { event, _, _, _ in - captureEvent(event) - } - } - - override init(mapping: @escaping EventMapping.Mapping) { - fatalError("Use init()") - } -} - -final class MockUserBehaviorStore: UserBehaviorStoring { - - var didRefreshTimestamp: Date? - var didDoubleRefreshTimestamp: Date? - var didRefreshCounter: Int = 0 - -} - -final class UserBehaviorMonitorTests: XCTestCase { - - var eventMapping: MockUserBehaviorEventsMapping! - var monitor: UserBehaviorMonitor! - var events: [UserBehaviorEvent] = [] - - override func setUp() { - super.setUp() - events.removeAll() - eventMapping = MockUserBehaviorEventsMapping(captureEvent: { event in - self.events.append(event) - }) - monitor = UserBehaviorMonitor(eventMapping: eventMapping, - store: MockUserBehaviorStore()) - } - - // - MARK: Behavior testing - // Expecting events - - func testWhenUserRefreshesTwiceItSendsReloadTwiceEvent() { - monitor.handleRefreshAction() - monitor.handleRefreshAction() - XCTAssertEqual(events.count, 1) - XCTAssertEqual(events[0], .reloadTwiceWithin12Seconds) - } - - func testWhenUserRefreshesThreeTimesItSendsTwoReloadTwiceEvents() { - monitor.handleRefreshAction() - monitor.handleRefreshAction() - monitor.handleRefreshAction() - XCTAssertEqual(events.count, 3) - XCTAssertEqual(events[0], .reloadTwiceWithin12Seconds) - XCTAssertEqual(events[1], .reloadTwiceWithin12Seconds) - } - - func testWhenUserRefreshesThreeTimesItSendsReloadThreeTimesEvent() { - monitor.handleRefreshAction() - monitor.handleRefreshAction() - monitor.handleRefreshAction() - XCTAssertEqual(events.count, 3) - XCTAssertEqual(events[2], .reloadThreeTimesWithin20Seconds) - } - - // Timed pixels - - func testReloadTwiceEventShouldNotSendEventIfSecondRefreshOccuredAfter12Seconds() { - let date = Date() - monitor.handleRefreshAction(date: date) - monitor.handleRefreshAction(date: date + 13) // 13 seconds after the first event - XCTAssertTrue(events.isEmpty) - } - - func testReloadTwiceEventShouldSendEventIfSecondRefreshOccurredBelow12Seconds() { - let date = Date() - monitor.handleRefreshAction(date: date) - monitor.handleRefreshAction(date: date + 11) // 20 seconds after the first event - XCTAssertEqual(events.count, 1) - XCTAssertEqual(events[0], .reloadTwiceWithin12Seconds) - } - - func testReloadThreeTimesEventShouldNotSendEventIfThreeRefreshesOccurredAfter20Seconds() { - let date = Date() - monitor.handleRefreshAction(date: date) - monitor.handleRefreshAction(date: date) - monitor.handleRefreshAction(date: date + 21) // 21 seconds after the first event - events.removeAll { $0 == .reloadTwiceWithin12Seconds } // remove events that are not being tested - XCTAssertTrue(events.isEmpty) - } - - func testReloadThreeTimesEventShouldSendEventIfThreeRefreshesOccurredBelow20Seconds() { - let date = Date() - monitor.handleRefreshAction(date: date) - monitor.handleRefreshAction(date: date) - monitor.handleRefreshAction(date: date + 19) // 10 seconds after the first event - events.removeAll { $0 == .reloadTwiceWithin12Seconds } // remove events that are not being tested - XCTAssertEqual(events.count, 1) - XCTAssertEqual(events[0], .reloadThreeTimesWithin20Seconds) - } - -} From becebe95961e2aff8d7560e6f681454972a52a41 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 11 Nov 2024 11:41:14 +0100 Subject: [PATCH 4/7] [DuckPlayer] Base Overlay Pixel Implementation (#3545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204099484721401/1208686031091507/f Tech Design URL: CC: --- Core/PixelEvent.swift | 18 ++ DuckDuckGo.xcodeproj/project.pbxproj | 8 + .../DuckPlayerNavigationHandler.swift | 10 +- .../DuckPlayerNavigationHandling.swift | 3 + .../DuckPlayerOverlayUsagePixels.swift | 123 ++++++++++ .../DuckPlayerOverlayUsagePixelsTests.swift | 227 ++++++++++++++++++ 6 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift create mode 100644 DuckDuckGoTests/DuckPlayerOverlayUsagePixelsTests.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 35429325fc..2589576efc 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -850,6 +850,14 @@ extension Pixel { case protectedDataUnavailableWhenBecomeActive case statisticsLoaderATBStateMismatch case adAttributionReportStateMismatch + + // MARK: - DuckPlayer Overlay Navigation + case duckPlayerYouTubeOverlayNavigationBack + case duckPlayerYouTubeOverlayNavigationRefresh + case duckPlayerYouTubeNavigationWithinYouTube + case duckPlayerYouTubeOverlayNavigationOutsideYoutube + case duckPlayerYouTubeOverlayNavigationClosed + case duckPlayerYouTubeNavigationIdle30 } } @@ -1690,6 +1698,16 @@ extension Pixel.Event { case .protectedDataUnavailableWhenBecomeActive: return "m_protected_data_unavailable_when_become_active" case .statisticsLoaderATBStateMismatch: return "m_statistics_loader_atb_state_mismatch" case .adAttributionReportStateMismatch: return "m_ad_attribution_report_state_mismatch" + + // MARK: - DuckPlayer Overlay Navigation + case .duckPlayerYouTubeOverlayNavigationBack: return "duckplayer.youtube.overlay.navigation.back" + case .duckPlayerYouTubeOverlayNavigationRefresh: return "duckplayer.youtube.overlay.navigation.refresh" + case .duckPlayerYouTubeNavigationWithinYouTube: return "duckplayer.youtube.overlay.navigation.within-youtube" + case .duckPlayerYouTubeOverlayNavigationOutsideYoutube: return "duckplayer.youtube.overlay.navigation.outside-youtube" + case .duckPlayerYouTubeOverlayNavigationClosed: return "duckplayer.youtube.overlay.navigation.closed" + case .duckPlayerYouTubeNavigationIdle30: return "duckplayer.youtube.overlay.idle-30" + + } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ea3eb46242..58e5dfe98e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1014,6 +1014,8 @@ D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; D6ACEA322BBD55BF008FADDF /* TabURLInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6ACEA312BBD55BF008FADDF /* TabURLInterceptor.swift */; }; D6B67A122C332B6E002122EB /* DuckPlayerMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B67A112C332B6E002122EB /* DuckPlayerMocks.swift */; }; + D6B9E8D22CDA4420002B640C /* DuckPlayerOverlayUsagePixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9E8D12CDA4418002B640C /* DuckPlayerOverlayUsagePixels.swift */; }; + D6B9E8D42CDA8375002B640C /* DuckPlayerOverlayUsagePixelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9E8D32CDA8369002B640C /* DuckPlayerOverlayUsagePixelsTests.swift */; }; D6BC8ACB2C5AA3860025375B /* DuckPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = D6BC8ACA2C5AA3860025375B /* DuckPlayer */; }; D6BFCB5F2B7524AA0051FF81 /* SubscriptionPIRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */; }; D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */; }; @@ -2823,6 +2825,8 @@ D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; D6ACEA312BBD55BF008FADDF /* TabURLInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabURLInterceptor.swift; sourceTree = ""; }; D6B67A112C332B6E002122EB /* DuckPlayerMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerMocks.swift; sourceTree = ""; }; + D6B9E8D12CDA4418002B640C /* DuckPlayerOverlayUsagePixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerOverlayUsagePixels.swift; sourceTree = ""; }; + D6B9E8D32CDA8369002B640C /* DuckPlayerOverlayUsagePixelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerOverlayUsagePixelsTests.swift; sourceTree = ""; }; D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRView.swift; sourceTree = ""; }; D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRViewModel.swift; sourceTree = ""; }; D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHeadlessWebViewModel.swift; sourceTree = ""; }; @@ -5324,6 +5328,7 @@ D62EC3B72C24695800FC9D04 /* DuckPlayer */ = { isa = PBXGroup; children = ( + D6B9E8D32CDA8369002B640C /* DuckPlayerOverlayUsagePixelsTests.swift */, D6B67A112C332B6E002122EB /* DuckPlayerMocks.swift */, D62EC3BB2C2470E000FC9D04 /* DuckPlayerTests.swift */, D62EC3B82C246A5600FC9D04 /* YoutublePlayerNavigationHandlerTests.swift */, @@ -5342,6 +5347,7 @@ D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */, D63FF8942C1B67E8006DE24D /* YoutubeOverlayUserScript.swift */, D63FF8932C1B67E8006DE24D /* YoutubePlayerUserScript.swift */, + D6B9E8D12CDA4418002B640C /* DuckPlayerOverlayUsagePixels.swift */, 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */, ); path = DuckPlayer; @@ -7471,6 +7477,7 @@ D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */, 6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */, C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */, + D6B9E8D22CDA4420002B640C /* DuckPlayerOverlayUsagePixels.swift in Sources */, 4B53648A26718D0E001AA041 /* EmailWaitlist.swift in Sources */, D63677F52BBDB1C300605BA5 /* DaxLogoNavbarTitle.swift in Sources */, 8524CC98246D66E100E59D45 /* String+Markdown.swift in Sources */, @@ -8001,6 +8008,7 @@ 5694372B2BE3F2D900C0881B /* SyncErrorHandlerTests.swift in Sources */, 4B27FBB52C927435007E21A7 /* PersistentPixelTests.swift in Sources */, 987130C7294AAB9F00AB05E0 /* MenuBookmarksViewModelTests.swift in Sources */, + D6B9E8D42CDA8375002B640C /* DuckPlayerOverlayUsagePixelsTests.swift in Sources */, 858650D32469BFAD00C36F8A /* DaxDialogTests.swift in Sources */, 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */, 31C138B227A4097800FFD4B2 /* DownloadTestsHelper.swift in Sources */, diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift index adb9d58ab0..f64a61f558 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift @@ -32,6 +32,9 @@ final class DuckPlayerNavigationHandler: NSObject { /// The DuckPlayer instance used for handling video playback. var duckPlayer: DuckPlayerControlling + /// The DuckPlayerOverlayPixelFiring instance used for handling overlay pixel firing. + var duckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring? + /// Indicates where the DuckPlayer was referred from (e.g., YouTube, SERP). var referrer: DuckPlayerReferrer = .other @@ -113,13 +116,15 @@ final class DuckPlayerNavigationHandler: NSObject { appSettings: AppSettings, pixelFiring: PixelFiring.Type = Pixel.self, dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self, - tabNavigationHandler: DuckPlayerTabNavigationHandling? = nil) { + tabNavigationHandler: DuckPlayerTabNavigationHandling? = nil, + duckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring? = nil) { self.duckPlayer = duckPlayer self.featureFlagger = featureFlagger self.appSettings = appSettings self.pixelFiring = pixelFiring self.dailyPixelFiring = dailyPixelFiring self.tabNavigationHandler = tabNavigationHandler + self.duckPlayerOverlayUsagePixels = duckPlayerOverlayUsagePixels } /// Returns the file path for the Duck Player HTML template. @@ -658,6 +663,9 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling { @MainActor func handleURLChange(webView: WKWebView) -> DuckPlayerNavigationHandlerURLChangeResult { + // Track overlayUsagePixels + duckPlayerOverlayUsagePixels?.registerNavigation(url: webView.url) + // We want to prevent multiple simultaneous redirects // This can be caused by Duplicate Nav events, and quick URL changes if let lastTimestamp = lastURLChangeHandling, diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandling.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandling.swift index 1755a54cf2..168512c112 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandling.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandling.swift @@ -74,6 +74,9 @@ protocol DuckPlayerNavigationHandling: AnyObject { /// The DuckPlayer instance used for handling video playback. var duckPlayer: DuckPlayerControlling { get } + /// DuckPlayerOverlayUsagePixels instance used for handling pixel firing. + var duckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring? { get } + /// Handles URL changes in the web view. /// /// - Parameter webView: The web view where the URL change occurred. diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift b/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift new file mode 100644 index 0000000000..459b762f40 --- /dev/null +++ b/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift @@ -0,0 +1,123 @@ +// +// DuckPlayerOverlayUsagePixels.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 + +protocol DuckPlayerOverlayPixelFiring { + + var pixelFiring: PixelFiring.Type { get set } + var navigationHistory: [URL] { get set } + + func registerNavigation(url: URL?) + func navigationBack(duckPlayerMode: DuckPlayerMode) + func navigationReload(duckPlayerMode: DuckPlayerMode) + func navigationWithinYoutube(duckPlayerMode: DuckPlayerMode) + func navigationOutsideYoutube(duckPlayerMode: DuckPlayerMode) + func navigationClosed(duckPlayerMode: DuckPlayerMode) + func overlayIdle(duckPlayerMode: DuckPlayerMode) + +} + +final class DuckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring { + + var pixelFiring: PixelFiring.Type + var navigationHistory: [URL] = [] + + private var idleTimer: Timer? + private var idleTimeInterval: TimeInterval + + init(pixelFiring: PixelFiring.Type = Pixel.self, + navigationHistory: [URL] = [], + timeoutInterval: TimeInterval = 30.0) { + self.pixelFiring = pixelFiring + self.idleTimeInterval = timeoutInterval + } + + // Method to reset the idle timer + private func resetIdleTimer() { + idleTimer?.invalidate() + idleTimer = nil + } + + func registerNavigation(url: URL?) { + guard let url = url else { return } + navigationHistory.append(url) + + // Cancel and reset the idle timer whenever a new navigation occurs + resetIdleTimer() + } + + func navigationBack(duckPlayerMode: DuckPlayerMode) { + guard duckPlayerMode == .alwaysAsk, + let lastURL = navigationHistory.last, + lastURL.isYoutubeWatch else { return } + + pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationBack, withAdditionalParameters: [:]) + } + + func navigationReload(duckPlayerMode: DuckPlayerMode) { + guard duckPlayerMode == .alwaysAsk, + let lastURL = navigationHistory.last, + lastURL.isYoutubeWatch else { return } + + pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationRefresh, withAdditionalParameters: [:]) + } + + func navigationWithinYoutube(duckPlayerMode: DuckPlayerMode) { + guard duckPlayerMode == .alwaysAsk, + navigationHistory.count > 1, + let currentURL = navigationHistory.last, + let previousURL = navigationHistory.dropLast().last, + previousURL.isYoutubeWatch, + currentURL.isYoutube else { return } + + pixelFiring.fire(.duckPlayerYouTubeNavigationWithinYouTube, withAdditionalParameters: [:]) + } + + func navigationOutsideYoutube(duckPlayerMode: DuckPlayerMode) { + guard duckPlayerMode == .alwaysAsk, + navigationHistory.count > 1, + let currentURL = navigationHistory.last, + let previousURL = navigationHistory.dropLast().last, + previousURL.isYoutubeWatch, + !currentURL.isYoutube else { return } + + pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationOutsideYoutube, withAdditionalParameters: [:]) + } + + func navigationClosed(duckPlayerMode: DuckPlayerMode) { + + guard duckPlayerMode == .alwaysAsk, + let lastURL = navigationHistory.last, + lastURL.isYoutubeWatch else { return } + + pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationClosed, withAdditionalParameters: [:]) + + } + + func overlayIdle(duckPlayerMode: DuckPlayerMode) { + guard duckPlayerMode == .alwaysAsk, + let lastURL = navigationHistory.last, + lastURL.isYoutubeWatch else { return } + + idleTimer = Timer.scheduledTimer(withTimeInterval: idleTimeInterval, repeats: false) { [weak self] _ in + self?.pixelFiring.fire(.duckPlayerYouTubeNavigationIdle30, withAdditionalParameters: [:]) + } + } +} diff --git a/DuckDuckGoTests/DuckPlayerOverlayUsagePixelsTests.swift b/DuckDuckGoTests/DuckPlayerOverlayUsagePixelsTests.swift new file mode 100644 index 0000000000..dc5be8e048 --- /dev/null +++ b/DuckDuckGoTests/DuckPlayerOverlayUsagePixelsTests.swift @@ -0,0 +1,227 @@ +// +// DuckPlayerOverlayUsagePixelsTests.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 Core +@testable import DuckDuckGo + +class DuckPlayerOverlayUsagePixelsTests: XCTestCase { + + var duckPlayerOverlayPixels: DuckPlayerOverlayUsagePixels! + + override func setUp() { + super.setUp() + // Initialize DuckPlayerOverlayUsagePixels with a shorter timeoutInterval for testing + PixelFiringMock.tearDown() + duckPlayerOverlayPixels = DuckPlayerOverlayUsagePixels(pixelFiring: PixelFiringMock.self, timeoutInterval: 3.0) + } + + override func tearDown() { + // Clean up after each test + PixelFiringMock.tearDown() + duckPlayerOverlayPixels = nil + super.tearDown() + } + + func testRegisterNavigationAddsURLToHistory() { + // Arrange + let testURL = URL(string: "https://www.example.com")! + + // Act + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Assert + XCTAssertEqual(duckPlayerOverlayPixels.navigationHistory.count, 1) + XCTAssertEqual(duckPlayerOverlayPixels.navigationHistory.first, testURL) + } + + func testRegisterNavigationWithNilURLDoesNotAddToHistory() { + // Act + duckPlayerOverlayPixels.registerNavigation(url: nil) + + // Assert + XCTAssertTrue(duckPlayerOverlayPixels.navigationHistory.isEmpty, "Navigation history should remain empty when registering a nil URL.") + } + + func testNavigationBackFiresPixelWhenConditionsMet() { + // Arrange + let testURL = URL(string: "https://www.youtube.com/watch?v=example")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.navigationBack(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.duckPlayerYouTubeOverlayNavigationBack.name) + XCTAssertNotNil(PixelFiringMock.lastPixelInfo, "Pixel should be fired when conditions are met for navigationBack.") + } + + func testNavigationBackDoesNotFirePixelWhenConditionsNotMet() { + // Act + duckPlayerOverlayPixels.navigationBack(duckPlayerMode: .enabled) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired when conditions are not met for navigationBack.") + } + + func testNavigationReloadFiresPixelWhenConditionsMet() { + // Arrange + let testURL = URL(string: "https://www.youtube.com/watch?v=example")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.navigationReload(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.duckPlayerYouTubeOverlayNavigationRefresh.name) + XCTAssertNotNil(PixelFiringMock.lastPixelInfo, "Pixel should be fired when conditions are met for navigationReload.") + } + + func testNavigationReloadDoesNotFirePixelWhenConditionsNotMet() { + // Act + duckPlayerOverlayPixels.navigationReload(duckPlayerMode: .enabled) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired when conditions are not met for navigationReload.") + } + + func testNavigationWithinYoutubeFiresPixelWhenConditionsMet() { + // Arrange + let previousURL = URL(string: "https://www.youtube.com/watch?v=example1")! + let currentURL = URL(string: "https://www.youtube.com/watch?v=example2")! + duckPlayerOverlayPixels.registerNavigation(url: previousURL) + duckPlayerOverlayPixels.registerNavigation(url: currentURL) + + // Act + duckPlayerOverlayPixels.navigationWithinYoutube(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.duckPlayerYouTubeNavigationWithinYouTube.name) + XCTAssertNotNil(PixelFiringMock.lastPixelInfo, "Pixel should be fired when conditions are met for navigationWithinYoutube.") + } + + func testNavigationWithinYoutubeDoesNotFirePixelWhenConditionsNotMet() { + // Arrange + let testURL = URL(string: "https://www.example.com")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.navigationWithinYoutube(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired when conditions are not met for navigationWithinYoutube.") + } + + func testNavigationOutsideYoutubeFiresPixelWhenConditionsMet() { + // Arrange + let previousURL = URL(string: "https://www.youtube.com/watch?v=example1")! + let currentURL = URL(string: "https://www.example.com")! + duckPlayerOverlayPixels.registerNavigation(url: previousURL) + duckPlayerOverlayPixels.registerNavigation(url: currentURL) + + // Act + duckPlayerOverlayPixels.navigationOutsideYoutube(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.duckPlayerYouTubeOverlayNavigationOutsideYoutube.name) + XCTAssertNotNil(PixelFiringMock.lastPixelInfo, "Pixel should be fired when conditions are met for navigationOutsideYoutube.") + } + + func testNavigationOutsideYoutubeDoesNotFirePixelWhenConditionsNotMet() { + // Arrange + let testURL = URL(string: "https://www.youtube.com/watch?v=example")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.navigationOutsideYoutube(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired when conditions are not met for navigationOutsideYoutube.") + } + + func testOverlayIdleStartsTimerAndFiresPixelAfter3Seconds() { + // Arrange + let testURL = URL(string: "https://www.youtube.com/watch?v=example")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.overlayIdle(duckPlayerMode: .alwaysAsk) + + // Simulate waiting for 3 seconds + let expectation = XCTestExpectation(description: "Wait for the pixel to be fired after 3 seconds.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3.1) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 5) + + // Assert + XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.duckPlayerYouTubeNavigationIdle30.name) + XCTAssertNotNil(PixelFiringMock.lastPixelInfo, "Pixel should be fired after 3 seconds of inactivity.") + } + + func testOverlayIdleDoesNotFirePixelWhenNavigationHistoryIsNotYouTubeWatch() { + // Arrange + let testURL = URL(string: "https://www.example.com")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.overlayIdle(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired if the last URL is not a YouTube watch URL.") + } + + func testOverlayIdleDoesNotStartTimerIfModeIsNotAlwaysAsk() { + // Arrange + let testURL = URL(string: "https://www.youtube.com/watch?v=example")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.overlayIdle(duckPlayerMode: .enabled) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired if the mode is not .alwaysAsk.") + } + + func testNavigationClosedFiresPixelWhenConditionsMet() { + // Arrange + let testURL = URL(string: "https://www.youtube.com/watch?v=example")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.navigationClosed(duckPlayerMode: .alwaysAsk) + + // Assert + XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.duckPlayerYouTubeOverlayNavigationClosed.name) + XCTAssertNotNil(PixelFiringMock.lastPixelInfo, "Pixel should be fired when conditions are met for navigationReload.") + } + + func testNavigationClosedDoesNotFirePixelWhenConditionsNotMet() { + // Arrange + let testURL = URL(string: "https://www.youtube.com")! + duckPlayerOverlayPixels.registerNavigation(url: testURL) + + // Act + duckPlayerOverlayPixels.navigationClosed(duckPlayerMode: .enabled) + + // Assert + XCTAssertNil(PixelFiringMock.lastPixelName, "Pixel should not be fired when conditions are not met for navigationClosed.") + } +} From e8b0c2b7b525a81ea5c1f3d3922e9aa72f3a7497 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Mon, 11 Nov 2024 14:49:34 +0000 Subject: [PATCH 5/7] Release 7.145.0-0 (#3560) --- .github/workflows/pr.yml | 2 +- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 98 +++++++++++++++++-- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++------ DuckDuckGo/Settings.bundle/Root.plist | 2 +- 6 files changed, 122 insertions(+), 42 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 87a40966e1..2248bd73f8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -40,7 +40,7 @@ jobs: name: Unit Tests runs-on: macos-14-xlarge - timeout-minutes: 15 + timeout-minutes: 20 outputs: commit_author: ${{ steps.fetch_commit_author.outputs.commit_author }} diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 40ae091e7d..a31bce32d5 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.144.0 +MARKETING_VERSION = 7.145.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index 3036ae8a6a..b83c4fa194 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 = "\"516f95a16f7a556c58e14ee6f193cc30\"" - public static let embeddedDataSHA = "87314e1ac02784472a722844a27b443b0387a164ac72afaac00d9a70731fc572" + public static let embeddedDataETag = "\"bd1f5490770791f68fa9667d530758a9\"" + public static let embeddedDataSHA = "86b8c31a53f781d66dbc16eb09578d28149fee6c0e280fc75ddafeed8a4b46ac" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index b7d7ecc0d0..d04ad45857 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1730481067679, + "version": 1731320660413, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -1357,14 +1357,11 @@ { "domain": "flexmls.com" }, - { - "domain": "centerwellpharmacy.com" - }, { "domain": "instructure.com" } ], - "hash": "1cc80acd10d985c950e40c5b876c581b" + "hash": "96b2f778bab196aa424e9c859ddea778" }, "contextualOnboarding": { "exceptions": [], @@ -1424,6 +1421,12 @@ { "domain": "instructure.com" }, + { + "domain": "humana.com" + }, + { + "domain": "centerwellpharmacy.com" + }, { "domain": "marvel.com" }, @@ -1441,7 +1444,7 @@ } ], "state": "disabled", - "hash": "fce0a9ccd7ae060d25e7debe4d8905fb" + "hash": "33c2d56a2a9dd43c88a353d8a57dfa72" }, "customUserAgent": { "settings": { @@ -1802,6 +1805,10 @@ "selector": ".top-ad", "type": "hide-empty" }, + { + "selector": "#top-ad", + "type": "hide" + }, { "selector": "#topAd", "type": "hide-empty" @@ -1854,6 +1861,10 @@ "selector": "#google-one-tap-popup-container", "type": "hide" }, + { + "selector": ".google-one-tap__module", + "type": "hide" + }, { "selector": ".google-one-tap-modal-div", "type": "hide" @@ -2165,6 +2176,7 @@ "content continues below", "x", "_", + "sponsor message", "sponsored", "sponsorisé", "story continues below advertisement", @@ -3227,6 +3239,10 @@ { "selector": "[aria-labelledby='promo-header']", "type": "hide" + }, + { + "selector": "div[role='banner']:has(div > a[href='https://support.google.com/a/answer/33864'])", + "type": "hide" } ] }, @@ -4965,7 +4981,7 @@ ] }, "state": "enabled", - "hash": "d8fb8089fcfbd527940703c8e2665966" + "hash": "2fa4c7c9bfd50585ee22d6c2d3bd3279" }, "exceptionHandler": { "exceptions": [ @@ -5638,10 +5654,13 @@ }, "userTips": { "state": "enabled" + }, + "enforceRoutes": { + "state": "enabled" } }, "exceptions": [], - "hash": "324309d731591edf4174a6e5d11b837c" + "hash": "137177ca43449da8ce870fc838b35845" }, "newTabContinueSetUp": { "exceptions": [], @@ -5917,6 +5936,27 @@ "state": "disabled", "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, + "showOnAppLaunch": { + "exceptions": [ + { + "domain": "marvel.com" + }, + { + "domain": "sundancecatalog.com" + }, + { + "domain": "noaprints.com" + }, + { + "domain": "flexmls.com" + }, + { + "domain": "instructure.com" + } + ], + "state": "disabled", + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" + }, "sslCertificates": { "state": "enabled", "exceptions": [], @@ -5927,6 +5967,27 @@ }, "hash": "abe9584048f7f8157f71a14e7914cb1c" }, + "swipingTabs": { + "exceptions": [ + { + "domain": "marvel.com" + }, + { + "domain": "sundancecatalog.com" + }, + { + "domain": "noaprints.com" + }, + { + "domain": "flexmls.com" + }, + { + "domain": "instructure.com" + } + ], + "state": "disabled", + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" + }, "syncPromotion": { "state": "enabled", "features": { @@ -6765,6 +6826,12 @@ "homedepot.com", "sbs.com.au" ] + }, + { + "rule": "sbs.demdex.net", + "domains": [ + "sbs.com.au" + ] } ] }, @@ -6863,6 +6930,12 @@ "wunderground.com" ] }, + { + "rule": "securepubads.g.doubleclick.net/pagead/ima_ppub_config", + "domains": [ + "sbs.com.au" + ] + }, { "rule": "securepubads.g.doubleclick.net/pagead/managed/js/gpt", "domains": [ @@ -8944,6 +9017,13 @@ "" ] }, + { + "rule": "tags.tiqcdn.com/utag/cbsi/", + "domains": [ + "cbs.com", + "paramountplus.com" + ] + }, { "rule": "tags.tiqcdn.com/utag/", "domains": [ @@ -9347,7 +9427,7 @@ "domain": "instructure.com" } ], - "hash": "c28128dee65a2aa7fef1528b73f33c7f" + "hash": "b7c276ffe1417313a46c0d13fbc9fcd9" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 58e5dfe98e..7b12570caa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9195,7 +9195,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9232,7 +9232,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9322,7 +9322,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9349,7 +9349,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9498,7 +9498,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 = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9523,7 +9523,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 = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9592,7 +9592,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9626,7 +9626,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9659,7 +9659,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9689,7 +9689,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9999,7 +9999,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 = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10030,7 +10030,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10058,7 +10058,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10091,7 +10091,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10121,7 +10121,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10154,11 +10154,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10391,7 +10391,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 = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10418,7 +10418,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10450,7 +10450,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10487,7 +10487,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10522,7 +10522,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10557,11 +10557,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10734,11 +10734,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10767,10 +10767,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 6e1c6ef73b..cac348ca4f 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.144.0 + 7.145.0 Key version Title From e6e1ec368d44256a8890d94e03ee501ef99c8f00 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 11 Nov 2024 16:55:16 +0100 Subject: [PATCH 6/7] Sync: Send pixels for account removal + decoding issues (#3557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201493110486074/1208723035104886/f Tech Design URL: CC: **Description**: Adds sync pixels for catching when the keychain read throws a decoding error and when the account is deleted for various reasons **Steps to test this PR**: 1. Make sure you've already activated sync. 2. Deactivate sync (either delete the server data or just turn it off) 3. You should see the `sync_account_removed_reason_user-turned-off` pixel in the console. **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/PixelEvent.swift | 4 ++++ Core/SyncErrorHandler.swift | 4 ++++ DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2589576efc..2ddc0806ca 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -632,6 +632,8 @@ extension Pixel { case syncDeleteAccountError case syncLoginExistingAccountError case syncSecureStorageReadError + case syncSecureStorageDecodingError + case syncAccountRemoved(reason: String) case syncGetOtherDevices case syncGetOtherDevicesCopy @@ -1457,6 +1459,8 @@ extension Pixel.Event { case .syncDeleteAccountError: return "m_d_sync_delete_account_error" case .syncLoginExistingAccountError: return "m_d_sync_login_existing_account_error" case .syncSecureStorageReadError: return "m_d_sync_secure_storage_error" + case .syncSecureStorageDecodingError: return "sync_secure_storage_decoding_error" + case .syncAccountRemoved(let reason): return "sync_account_removed_reason_\(reason)" case .syncGetOtherDevices: return "sync_get_other_devices" case .syncGetOtherDevicesCopy: return "sync_get_other_devices_copy" diff --git a/Core/SyncErrorHandler.swift b/Core/SyncErrorHandler.swift index 93609732ba..38dce71464 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -102,6 +102,10 @@ public class SyncErrorHandler: EventMapping { Pixel.fire(pixel: .syncFailedToSetupEngine, error: error) case .failedToReadSecureStore: Pixel.fire(pixel: .syncSecureStorageReadError, error: error) + case .failedToDecodeSecureStoreData(let error): + Pixel.fire(pixel: .syncSecureStorageDecodingError, error: error) + case .accountRemoved(let reason): + Pixel.fire(pixel: .syncAccountRemoved(reason: reason.rawValue), error: error) default: // Should this be so generic? let domainEvent = Pixel.Event.syncSentUnauthenticatedRequest diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 58e5dfe98e..e50066830b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10980,7 +10980,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 208.0.0; + version = 208.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 49d5d017ea..cb461c0e9c 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" : "17154907fe86c75942331ed6d037694c666ddd95", - "version" : "208.0.0" + "revision" : "6be781530a2516c703b8e1bcf0c90e6e763d3300", + "version" : "208.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "1733ee59f06f6e725a98cf6cd8322159f59d664b", - "version" : "6.31.0" + "revision" : "adca39c379b1a124f9990e9d0308c374f32f5018", + "version" : "6.32.0" } }, { From f8fc6c5a486ee86da777447e23cf70ab4a31fd30 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 11 Nov 2024 22:16:29 +0100 Subject: [PATCH 7/7] Remediate TunnelVision, TunnelCrack and fix "Exclude Local Networks" (#3460) Task/Issue URL: https://app.asana.com/0/1206580121312550/1208686409805161/f Tech Design URL: https://app.asana.com/0/481882893211075/1208643192597095/f macOS PR: https://github.com/duckduckgo/macos-browser/pull/3422 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/1039 ## Description Remediate TunnelVision, TunnelCrack and fix "Exclude Local Networks". --- Core/FeatureFlag.swift | 5 ++ DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../xcschemes/DuckDuckGo.xcscheme | 3 ++ DuckDuckGo/AppDependencyProvider.swift | 15 +++--- ...orkProtectionConvenienceInitialisers.swift | 1 + ...NetworkProtectionDebugViewController.swift | 12 +++++ .../NetworkProtectionTunnelController.swift | 52 +++++++++++++++++-- .../NetworkProtectionVPNSettingsView.swift | 3 -- ...etworkProtectionVPNSettingsViewModel.swift | 30 ++++++++--- 10 files changed, 103 insertions(+), 24 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index b877485851..de9c1df97e 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -49,6 +49,9 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/72649045549333/1208231259093710/f case networkProtectionUserTips + + /// https://app.asana.com/0/72649045549333/1208617860225199/f + case networkProtectionEnforceRoutes } extension FeatureFlag: FeatureFlagSourceProviding { @@ -104,6 +107,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.feature(.autocompleteTabs)) case .networkProtectionUserTips: return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.userTips)) + case .networkProtectionEnforceRoutes: + return .remoteDevelopment(.subfeature(NetworkProtectionSubfeature.enforceRoutes)) case .adAttributionReporting: return .remoteReleasable(.feature(.adAttributionReporting)) } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6f33031202..695adc1f30 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10980,7 +10980,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 208.1.0; + version = 209.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 cb461c0e9c..c542bb4b81 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" : "6be781530a2516c703b8e1bcf0c90e6e763d3300", - "version" : "208.1.0" + "revision" : "614ea57db48db644ce7f3a3de9c20c9a7fbb08ff", + "version" : "209.0.0" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index b9ef06c977..a096dee425 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -65,6 +65,9 @@ + + diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 35d7bf45d0..245901da44 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -113,7 +113,7 @@ final class AppDependencyProvider: DependencyProvider { entitlementsCache: entitlementsCache, subscriptionEndpointService: subscriptionService, authEndpointService: authService) - + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(), accountManager: accountManager, subscriptionEndpointService: subscriptionService, @@ -126,17 +126,14 @@ final class AppDependencyProvider: DependencyProvider { let accessTokenProvider: () -> String? = { return { accountManager.accessToken } }() -#if os(macOS) - networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), - serviceName: "\(Bundle.main.bundleIdentifier!).authToken", - errorEvents: .networkProtectionAppDebugEvents, - accessTokenProvider: accessTokenProvider) -#else + networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider) -#endif + networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager, tokenStore: networkProtectionKeychainTokenStore, - persistentPixel: persistentPixel) + featureFlagger: featureFlagger, + persistentPixel: persistentPixel, + settings: vpnSettings) vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, accountManager: accountManager) } diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index e440446044..a72c4f0ab0 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -59,6 +59,7 @@ extension NetworkProtectionVPNSettingsViewModel { convenience init() { self.init( notificationsAuthorization: NotificationsAuthorizationController(), + controller: AppDependencyProvider.shared.networkProtectionTunnelController, settings: AppDependencyProvider.shared.vpnSettings ) } diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index 0c0c34568e..fb38929911 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -63,6 +63,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { enum DebugFeatureRows: Int, CaseIterable { case toggleAlwaysOn + case enforceRoutes } enum SimulateFailureRows: Int, CaseIterable { @@ -324,6 +325,14 @@ final class NetworkProtectionDebugViewController: UITableViewController { } else { cell.accessoryType = .checkmark } + case .enforceRoutes: + cell.textLabel?.text = "Enforce Routes" + + if !AppDependencyProvider.shared.vpnSettings.enforceRoutes { + cell.accessoryType = .none + } else { + cell.accessoryType = .checkmark + } default: break } @@ -334,6 +343,9 @@ final class NetworkProtectionDebugViewController: UITableViewController { case .toggleAlwaysOn: debugFeatures.alwaysOnDisabled.toggle() tableView.reloadRows(at: [indexPath], with: .none) + case .enforceRoutes: + AppDependencyProvider.shared.vpnSettings.enforceRoutes.toggle() + tableView.reloadRows(at: [indexPath], with: .none) default: break } diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index c13dedcb06..ed22d7cd91 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -17,9 +17,10 @@ // limitations under the License. // -import Foundation +import BrowserServicesKit import Combine import Core +import Foundation import NetworkExtension import NetworkProtection import Subscription @@ -34,6 +35,7 @@ enum VPNConfigurationRemovalReason: String { final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { static var shouldSimulateFailure: Bool = false + private let featureFlagger: FeatureFlagger private var internalManager: NETunnelProviderManager? private let debugFeatures = NetworkProtectionDebugFeatures() private let tokenStore: NetworkProtectionKeychainTokenStore @@ -42,6 +44,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private let notificationCenter: NotificationCenter = .default private var previousStatus: NEVPNStatus = .invalid private let persistentPixel: PersistentPixelFiring + private let settings: VPNSettings private var cancellables = Set() // MARK: - Manager, Session, & Connection @@ -119,9 +122,25 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } - init(accountManager: AccountManager, tokenStore: NetworkProtectionKeychainTokenStore, persistentPixel: PersistentPixelFiring) { - self.tokenStore = tokenStore + // MARK: - Enforce Routes + + private var enforceRoutes: Bool { + featureFlagger.isFeatureOn(.networkProtectionEnforceRoutes) + } + + // MARK: - Initializers + + init(accountManager: AccountManager, + tokenStore: NetworkProtectionKeychainTokenStore, + featureFlagger: FeatureFlagger, + persistentPixel: PersistentPixelFiring, + settings: VPNSettings) { + + self.featureFlagger = featureFlagger self.persistentPixel = persistentPixel + self.settings = settings + self.tokenStore = tokenStore + subscribeToSnoozeTimingChanges() subscribeToStatusChanges() subscribeToConfigurationChanges() @@ -180,6 +199,16 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr tunnelManager.connection.stopVPNTunnel() } + func command(_ command: VPNCommand) async throws { + guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession(), + activeSession.status == .connected else { + + return + } + + try? await activeSession.sendProviderRequest(.command(command)) + } + func removeVPN(reason: VPNConfigurationRemovalReason) async { do { try await tunnelManager?.removeFromPreferences() @@ -293,6 +322,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr return tunnelManager } + @MainActor private func setupAndSave(_ tunnelManager: NETunnelProviderManager) async throws { setup(tunnelManager) @@ -319,6 +349,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Setups the tunnel manager if it's not set up already. /// + @MainActor private func setup(_ tunnelManager: NETunnelProviderManager) { tunnelManager.localizedDescription = "DuckDuckGo VPN" tunnelManager.isEnabled = true @@ -327,9 +358,24 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr let protocolConfiguration = NETunnelProviderProtocol() protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server + protocolConfiguration.providerConfiguration = [:] + // always-on protocolConfiguration.disconnectOnSleep = false + // Enforce routes + protocolConfiguration.enforceRoutes = enforceRoutes + + // We will control excluded networks through includedRoutes / excludedRoutes + protocolConfiguration.excludeLocalNetworks = false + + #if DEBUG + if #available(iOS 17.4, *) { + // This is useful to ensure debugging is never blocked by the VPN + protocolConfiguration.excludeDeviceCommunication = true + } + #endif + return protocolConfiguration }() diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift index 6d8ab17c8c..b01354f454 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift @@ -50,9 +50,6 @@ struct NetworkProtectionVPNSettingsView: View { footerText: UserText.netPExcludeLocalNetworksSettingFooter ) { Toggle("", isOn: $viewModel.excludeLocalNetworks) - .onTapGesture { - viewModel.toggleExcludeLocalNetworks() - } } dnsSection() diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift index 4866dd6055..9442ff6597 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift @@ -29,6 +29,7 @@ enum NetworkProtectionNotificationsViewKind: Equatable { } final class NetworkProtectionVPNSettingsViewModel: ObservableObject { + private let controller: TunnelController private let settings: VPNSettings private var cancellables: Set = [] @@ -39,11 +40,32 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject { self.settings.notifyStatusChanges } - @Published public var excludeLocalNetworks: Bool = true + @Published public var excludeLocalNetworks: Bool { + didSet { + guard oldValue != excludeLocalNetworks else { + return + } + + settings.excludeLocalNetworks = excludeLocalNetworks + + Task { + // We need to allow some time for the setting to propagate + // But ultimately this should actually be a user choice + try await Task.sleep(interval: 0.1) + try await controller.command(.restartAdapter) + } + } + } + @Published public var usesCustomDNS = false @Published public var dnsServers: String = UserText.vpnSettingDNSServerDefaultValue - init(notificationsAuthorization: NotificationsAuthorizationControlling, settings: VPNSettings) { + init(notificationsAuthorization: NotificationsAuthorizationControlling, + controller: TunnelController, + settings: VPNSettings) { + + self.controller = controller + self.excludeLocalNetworks = settings.excludeLocalNetworks self.settings = settings self.notificationsAuthorization = notificationsAuthorization @@ -77,10 +99,6 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject { settings.notifyStatusChanges = enabled } - func toggleExcludeLocalNetworks() { - settings.excludeLocalNetworks.toggle() - } - private static func localizedString(forRegionCode: String) -> String { Locale.current.localizedString(forRegionCode: forRegionCode) ?? forRegionCode.capitalized }