From 556b858a0b0c07c152183b06c5dd52d1262ed38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 4 Nov 2024 11:49:32 +0100 Subject: [PATCH 01/18] Add Privacy Config feature to control ad attribution reporting (#3506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1208638248015576/f Tech Design URL: CC: **Description**: Adds the ability to use remote config to control `AdAttributionPixelReporter` and whether the token is added as parameter. **Steps to test this PR**: ⚠️ Device is required to fully test this change. Attribution is not available on simulator. 1. Modify remote config URL to `https://www.jsonblob.com/api/1301173210350215168`. Put app in the background and reactivate. 2. Verify attribution pixel is fired including token parameter. 4. Remove the app, change `includeToken` setting to `false` in the linked configuration json file or remove setting object completely, verify attribution pixel is fired without token parameter. 5. Turn off the feature in configuration json, verify no attribution pixel is fired. **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/FeatureFlag.swift | 3 + DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../AdAttributionPixelReporter.swift | 34 +++++++++-- DuckDuckGo/AppDelegate.swift | 2 - .../AdAttributionPixelReporterTests.swift | 58 ++++++++++++++++++- 6 files changed, 91 insertions(+), 12 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 3a63769bb9..b877485851 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -45,6 +45,7 @@ public enum FeatureFlag: String { case onboardingAddToDock case autofillSurveys case autcompleteTabs + case adAttributionReporting /// https://app.asana.com/0/72649045549333/1208231259093710/f case networkProtectionUserTips @@ -103,6 +104,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.feature(.autocompleteTabs)) case .networkProtectionUserTips: return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.userTips)) + case .adAttributionReporting: + return .remoteReleasable(.feature(.adAttributionReporting)) } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 187e92d283..4a4cbaa73f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10970,7 +10970,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 203.0.0; + version = 203.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 80a835b202..0be6bd842d 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" : "45261df2963fc89094e169f9f2d0d9aa098093f3", - "version" : "203.0.0" + "revision" : "19f1e5c945aa92562ad2d087e8d6c99801edf656", + "version" : "203.1.0" } }, { diff --git a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift index a09eb9d693..c5c5f8a3cd 100644 --- a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift +++ b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift @@ -19,28 +19,37 @@ import Foundation import Core +import BrowserServicesKit final actor AdAttributionPixelReporter { - - static let isAdAttributionReportingEnabled = false - + static var shared = AdAttributionPixelReporter() private var fetcherStorage: AdAttributionReporterStorage private let attributionFetcher: AdAttributionFetcher + private let featureFlagger: FeatureFlagger + private let privacyConfigurationManager: PrivacyConfigurationManaging private let pixelFiring: PixelFiringAsync.Type private var isSendingAttribution: Bool = false init(fetcherStorage: AdAttributionReporterStorage = UserDefaultsAdAttributionReporterStorage(), attributionFetcher: AdAttributionFetcher = DefaultAdAttributionFetcher(), + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, pixelFiring: PixelFiringAsync.Type = Pixel.self) { self.fetcherStorage = fetcherStorage self.attributionFetcher = attributionFetcher self.pixelFiring = pixelFiring + self.featureFlagger = featureFlagger + self.privacyConfigurationManager = privacyConfigurationManager } @discardableResult func reportAttributionIfNeeded() async -> Bool { + guard featureFlagger.isFeatureOn(.adAttributionReporting) else { + return false + } + guard await fetcherStorage.wasAttributionReportSuccessful == false else { return false } @@ -57,7 +66,8 @@ final actor AdAttributionPixelReporter { if let (token, attributionData) = await self.attributionFetcher.fetch() { if attributionData.attribution { - let parameters = self.pixelParametersForAttribution(attributionData, attributionToken: token) + let settings = AdAttributionReporterSettings(privacyConfigurationManager.privacyConfig) + let parameters = self.pixelParametersForAttribution(attributionData, attributionToken: settings.includeToken ? token : nil) do { try await pixelFiring.fire( pixel: .appleAdAttribution, @@ -77,7 +87,7 @@ final actor AdAttributionPixelReporter { return false } - private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse, attributionToken: String) -> [String: String] { + private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse, attributionToken: String?) -> [String: String] { var params: [String: String] = [:] params[PixelParameters.adAttributionAdGroupID] = attribution.adGroupId.map(String.init) @@ -93,3 +103,17 @@ final actor AdAttributionPixelReporter { return params } } + +private struct AdAttributionReporterSettings { + var includeToken: Bool + + init(_ configuration: PrivacyConfiguration) { + let featureSettings = configuration.settings(for: .adAttributionReporting) + + self.includeToken = featureSettings[Key.includeToken] as? Bool ?? false + } + + private enum Key { + static let includeToken = "includeToken" + } +} diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index b5448d7b67..a585a0cbee 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -540,8 +540,6 @@ import os.log } private func reportAdAttribution() { - guard AdAttributionPixelReporter.isAdAttributionReportingEnabled else { return } - Task.detached(priority: .background) { await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() } diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift index 0a553d07b0..f8846346bd 100644 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift @@ -26,15 +26,24 @@ final class AdAttributionPixelReporterTests: XCTestCase { private var attributionFetcher: AdAttributionFetcherMock! private var fetcherStorage: AdAttributionReporterStorageMock! + private var featureFlagger: MockFeatureFlagger! + private var privacyConfigurationManager: PrivacyConfigurationManagerMock! override func setUpWithError() throws { attributionFetcher = AdAttributionFetcherMock() fetcherStorage = AdAttributionReporterStorageMock() + featureFlagger = MockFeatureFlagger() + privacyConfigurationManager = PrivacyConfigurationManagerMock() + + featureFlagger.enabledFeatureFlags.append(.adAttributionReporting) } override func tearDownWithError() throws { attributionFetcher = nil fetcherStorage = nil + featureFlagger = nil + privacyConfigurationManager = nil + PixelFiringMock.tearDown() } @@ -59,7 +68,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertFalse(result) } - func testPixelname() async { + func testPixelName() async { let sut = createSUT() attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) @@ -72,6 +81,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { func testPixelAttributesNaming() async throws { let sut = createSUT() attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + (privacyConfigurationManager.privacyConfig as? PrivacyConfigurationMock)?.settings[.adAttributionReporting] = ["includeToken": true] await sut.reportAttributionIfNeeded() @@ -157,9 +167,50 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertFalse(result) } + func testDoesNotReportIfFeatureDisabled() async { + let sut = createSUT() + attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + featureFlagger.enabledFeatureFlags = [] + + await fetcherStorage.markAttributionReportSuccessful() + let result = await sut.reportAttributionIfNeeded() + + XCTAssertNil(PixelFiringMock.lastPixelName) + XCTAssertFalse(result) + XCTAssertFalse(attributionFetcher.wasFetchCalled) + } + + func testDoesNotIncludeTokenWhenSettingMissing() async throws { + let sut = createSUT() + attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + featureFlagger.enabledFeatureFlags = [.adAttributionReporting] + + await sut.reportAttributionIfNeeded() + + let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) + + XCTAssertNil(pixelAttributes["attribution_token"]) + } + + func testIncludesTokenWhenSettingEnabled() async throws { + let sut = createSUT() + attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + featureFlagger.enabledFeatureFlags = [.adAttributionReporting] + + (privacyConfigurationManager.privacyConfig as? PrivacyConfigurationMock)?.settings[.adAttributionReporting] = ["includeToken": true] + + await sut.reportAttributionIfNeeded() + + let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) + + XCTAssertNotNil(pixelAttributes["attribution_token"]) + } + private func createSUT() -> AdAttributionPixelReporter { AdAttributionPixelReporter(fetcherStorage: fetcherStorage, attributionFetcher: attributionFetcher, + featureFlagger: featureFlagger, + privacyConfigurationManager: privacyConfigurationManager, pixelFiring: PixelFiringMock.self) } } @@ -173,9 +224,12 @@ class AdAttributionReporterStorageMock: AdAttributionReporterStorage { } class AdAttributionFetcherMock: AdAttributionFetcher { + var wasFetchCalled: Bool = false + var fetchResponse: (String, AdServicesAttributionResponse)? func fetch() async -> (String, AdServicesAttributionResponse)? { - fetchResponse + wasFetchCalled = true + return fetchResponse } } From 1c2abb9f69ebd76ebc3c45196bc7097fd286ac88 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 4 Nov 2024 12:51:08 +0100 Subject: [PATCH 02/18] Release 7.144.0-0 (#3528) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 238 ++++++++---------- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++--- DuckDuckGo/Settings.bundle/Root.plist | 2 +- fastlane/README.md | 8 + 6 files changed, 141 insertions(+), 169 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index e99711a2c3..40ae091e7d 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.143.0 +MARKETING_VERSION = 7.144.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index 15f2c36375..3036ae8a6a 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 = "\"f8b9cfd5f1eb7b77c21d4476f85bd177\"" - public static let embeddedDataSHA = "c26c97714d73a9e1e99dbd341d5890da42b49d34a296672be3d3cea00bdd37a0" + public static let embeddedDataETag = "\"516f95a16f7a556c58e14ee6f193cc30\"" + public static let embeddedDataSHA = "87314e1ac02784472a722844a27b443b0387a164ac72afaac00d9a70731fc572" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index edd16be36c..b7d7ecc0d0 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1730109523334, + "version": 1730481067679, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -93,9 +93,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -122,7 +119,7 @@ ] }, "state": "enabled", - "hash": "d9703d9553194bc54e66db1f2a4ec1f8" + "hash": "fa5f86bac5946c528cd6bc7449a2718a" }, "androidBrowserConfig": { "exceptions": [], @@ -363,6 +360,36 @@ { "domain": "la-becanerie.com" }, + { + "domain": "thrifty.com" + }, + { + "domain": "dollar.com" + }, + { + "domain": "ethicalconsumer.org" + }, + { + "domain": "diroots.com" + }, + { + "domain": "arbeitsagentur.de" + }, + { + "domain": "melawear.de" + }, + { + "domain": "dnb.com" + }, + { + "domain": "bookings.ltmuseum.co.uk" + }, + { + "domain": "famillemary.fr" + }, + { + "domain": "manoloblahnik.com" + }, { "domain": "marvel.com" }, @@ -377,9 +404,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -407,7 +431,7 @@ } } }, - "hash": "8392e127a3bcaee5c2913df355a7d254" + "hash": "c2885a67db26958bdb316564d5c94878" }, "autofillBreakageReporter": { "state": "enabled", @@ -505,12 +529,15 @@ }, { "percent": 50 + }, + { + "percent": 100 } ] } } }, - "hash": "9de8e4b066aa23f7c20ca638ee0d9f1a" + "hash": "91e54b0d57fbf1cf8668c9a929631432" }, "bookmarks": { "state": "enabled", @@ -534,12 +561,9 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "3766f6af346d3fffdf1e8ffce682c66e" + "hash": "37e0cf88badfc8b01b6394f0884502f6" }, "brokenSitePrompt": { "state": "enabled", @@ -1243,9 +1267,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -1263,7 +1284,7 @@ } }, "state": "disabled", - "hash": "3973e9d924c9a054df7f5dffad1f1d19" + "hash": "cb1f114a9e0314393b2a0f789cba163f" }, "clickToPlay": { "exceptions": [ @@ -1281,9 +1302,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -1296,7 +1314,7 @@ } }, "state": "disabled", - "hash": "31a06101df1dc362bfcef2d7a6320f80" + "hash": "894fb86c1f058aee9db47cfcdf3637de" }, "clientBrandHint": { "exceptions": [], @@ -1340,16 +1358,13 @@ "domain": "flexmls.com" }, { - "domain": "humana.com" + "domain": "centerwellpharmacy.com" }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "980bf875526f3cc7892c001a7d2e5a74" + "hash": "1cc80acd10d985c950e40c5b876c581b" }, "contextualOnboarding": { "exceptions": [], @@ -1372,6 +1387,10 @@ { "domain": "payments.google.com", "reason": "After sign-in for Google Pay flows (after flickering is resolved), blocking this causes the loading spinner to spin indefinitely, and the payment flow cannot proceed." + }, + { + "domain": "docs.google.com", + "reason": "Embedded Google docs get into redirect loop if signed into a Google account" } ], "firstPartyTrackerCookiePolicy": { @@ -1419,13 +1438,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "cef2b67a9df0d36b0875e7b54d33a4d0" + "hash": "fce0a9ccd7ae060d25e7debe4d8905fb" }, "customUserAgent": { "settings": { @@ -1446,6 +1462,10 @@ { "domain": "ihg.com", "reason": "https://github.com/duckduckgo/privacy-configuration/pull/2383" + }, + { + "domain": "humana.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/pull/2408" } ], "ddgDefaultSites": [ @@ -1475,7 +1495,7 @@ }, "exceptions": [], "state": "enabled", - "hash": "e577ccb473bdb7ada49c4d3c6e79cf01" + "hash": "345d837217e74afd3f9e5fd04b208fa7" }, "dbp": { "state": "disabled", @@ -1617,9 +1637,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -4169,6 +4186,15 @@ } ] }, + { + "domain": "salon.com", + "rules": [ + { + "selector": ".fc-ab-root", + "type": "hide" + } + ] + }, { "domain": "scmp.com", "rules": [ @@ -4939,7 +4965,7 @@ ] }, "state": "enabled", - "hash": "9518158b11d290809536a99f637f467e" + "hash": "d8fb8089fcfbd527940703c8e2665966" }, "exceptionHandler": { "exceptions": [ @@ -4957,13 +4983,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "a214254da3cc914ed5bfc0a2d893b589" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "extendedOnboarding": { "exceptions": [], @@ -4990,12 +5013,9 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "008c61cd03c28287a7f86b598c37078b" + "hash": "7f042650922da2636492e77ed1101bce" }, "fingerprintingBattery": { "exceptions": [ @@ -5016,13 +5036,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "d05606a02ffd6ce5e223bc26e748a203" + "hash": "fcc2138fa97c35ded544b39708fda919" }, "fingerprintingCanvas": { "settings": { @@ -5127,13 +5144,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "b0eef1a098ab8c6cc9d6da35a9cfb7ad" + "hash": "49a3d497835bf5715aaaa73f87dd974f" }, "fingerprintingHardware": { "settings": { @@ -5199,13 +5213,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "25a38bd7ccbca83ce0899548608235a7" + "hash": "cd4a8461973d1c1648dd20e6d1f532a7" }, "fingerprintingScreenSize": { "settings": { @@ -5259,13 +5270,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "c22a6e9f1c03693516589c47970d7a04" + "hash": "046340bb9287a20efed6189525ec5fed" }, "fingerprintingTemporaryStorage": { "exceptions": [ @@ -5292,13 +5300,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "48b1d8e96ee94825378d12a8d5a66895" + "hash": "14b7fe3d276b52109c59f0c71aee4f71" }, "googleRejected": { "exceptions": [ @@ -5316,13 +5321,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "a214254da3cc914ed5bfc0a2d893b589" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "gpc": { "state": "enabled", @@ -5371,9 +5373,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -5385,7 +5384,7 @@ "privacy-test-pages.site" ] }, - "hash": "37630ab090682ee7d004120a42031281" + "hash": "501bbc6471eb079cb27fa8a2a47467a5" }, "harmfulApis": { "settings": { @@ -5501,13 +5500,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "f255e336420119584b7000846be6d456" + "hash": "fb598c4167ff166d85dd49c701cc5579" }, "history": { "state": "enabled", @@ -5558,12 +5554,9 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "7407fc43cbd260f9aaca7cb7dab15bf4" + "hash": "b47d255c6f836ecb7ae0b3e61cc2c025" }, "incontextSignup": { "exceptions": [], @@ -5622,9 +5615,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -5635,7 +5625,7 @@ ] }, "state": "enabled", - "hash": "a1100eac5ecca0a11501df9f4dafa31a" + "hash": "d14f6e3a9aa4139ee1d517016b59691e" }, "networkProtection": { "state": "enabled", @@ -5682,13 +5672,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "5646a778c1cb6ec6e9c0da2c7dbd4bdb" + "hash": "82088db85ca7f64418fbfd57db25ade1" }, "performanceMetrics": { "state": "enabled", @@ -5707,12 +5694,9 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "60c3c3eed29e1e0c092fad8775483210" + "hash": "6792064606a5a72c5cd44addb4d40bda" }, "phishingDetection": { "state": "disabled", @@ -5731,12 +5715,9 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "3766f6af346d3fffdf1e8ffce682c66e" + "hash": "37e0cf88badfc8b01b6394f0884502f6" }, "pluginPointFocusedViewPlugin": { "state": "disabled", @@ -5853,13 +5834,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "68eb25a9461b134838100eecb0271905" + "hash": "138c3b2409f6b3bf967b804ab9bf2ce2" }, "remoteMessaging": { "state": "enabled", @@ -5883,15 +5861,12 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { "windowInMs": 0 }, - "hash": "13d2723b0c33943f086acb8c239e22e8" + "hash": "baf19d9e0f506ed09f46c95b1849adee" }, "runtimeChecks": { "state": "disabled", @@ -5910,13 +5885,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": {}, - "hash": "568cf394681d38683d1aeb8f0d0e6a7c" + "hash": "dfede9f06b9e322e198736703d013d15" }, "sendFullPackageInstallSource": { "state": "enabled", @@ -5940,13 +5912,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "a214254da3cc914ed5bfc0a2d893b589" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "sslCertificates": { "state": "enabled", @@ -5991,6 +5960,11 @@ "minSupportedVersion": "7.104.0", "hash": "d7dca6ee484eadebb5133e3f15fd9f41" }, + "textZoom": { + "exceptions": [], + "state": "enabled", + "hash": "52857469413a66e8b0c7b00de5589162" + }, "toggleReports": { "state": "enabled", "exceptions": [], @@ -6813,6 +6787,13 @@ "history.com" ] }, + { + "rule": "doubleclick.net/ondemand/dash/content/", + "domains": [ + "cbs.com", + "paramountplus.com" + ] + }, { "rule": "securepubads.g.doubleclick.net/gampad/ads", "domains": [ @@ -7425,7 +7406,8 @@ "piedmontng.com", "thesimsresource.com", "tradersync.com", - "vanguardplan.com" + "vanguardplan.com", + "xpn.org" ] } ] @@ -9363,12 +9345,9 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], - "hash": "434130223ee6493827d477d0171521da" + "hash": "c28128dee65a2aa7fef1528b73f33c7f" }, "trackingCookies1p": { "settings": { @@ -9392,13 +9371,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "a5c95510cb55fbe69cbff10e55a982dd" + "hash": "763f56424b0827b5731927a043219912" }, "trackingCookies3p": { "settings": { @@ -9419,13 +9395,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "5646a778c1cb6ec6e9c0da2c7dbd4bdb" + "hash": "82088db85ca7f64418fbfd57db25ade1" }, "trackingParameters": { "exceptions": [ @@ -9449,9 +9422,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "settings": { @@ -9484,7 +9454,7 @@ ] }, "state": "enabled", - "hash": "e530308726226930ff9a058fa064a39f" + "hash": "3805ecfb8a129f70a99e73a364b38f38" }, "userAgentRotation": { "settings": { @@ -9505,13 +9475,10 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "dd373ef0993c7ca9d9fa949db6d6aca0" + "hash": "9225b8785d6973db37abde99d81d219c" }, "voiceSearch": { "exceptions": [], @@ -9542,9 +9509,6 @@ }, { "domain": "instructure.com" - }, - { - "domain": "centerwellpharmacy.com" } ], "state": "enabled", @@ -9617,7 +9581,7 @@ } ] }, - "hash": "ed17f6ff342f200305eb4bbe544efec0" + "hash": "2853748f3ebb813d59f4db4a7bb13c83" }, "webViewBlobDownload": { "exceptions": [], diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4a4cbaa73f..42d05b4f6c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9185,7 +9185,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9222,7 +9222,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9312,7 +9312,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9339,7 +9339,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9488,7 +9488,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9513,7 +9513,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9582,7 +9582,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9616,7 +9616,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9649,7 +9649,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9679,7 +9679,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9989,7 +9989,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10020,7 +10020,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10048,7 +10048,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10081,7 +10081,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10111,7 +10111,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10144,11 +10144,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10381,7 +10381,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10408,7 +10408,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10440,7 +10440,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10477,7 +10477,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10512,7 +10512,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10547,11 +10547,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10724,11 +10724,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10757,10 +10757,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + 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 a7f84961db..6e1c6ef73b 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.143.0 + 7.144.0 Key version Title diff --git a/fastlane/README.md b/fastlane/README.md index 195f7f9607..bb3195509a 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -69,6 +69,14 @@ Makes Ad-Hoc build with a specified name and release bundle ID in a given direct Makes Ad-Hoc build for alpha with a specified name and alpha bundle ID in a given directory +### promote_latest_testflight_to_appstore + +```sh +[bundle exec] fastlane promote_latest_testflight_to_appstore +``` + +Promotes the latest TestFlight build to App Store without submitting for review + ### release_appstore ```sh From 2b49550f541eefbea2d6dde1ce4df3cc78f03692 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 4 Nov 2024 14:30:53 +0100 Subject: [PATCH 03/18] Update release notes (#3529) Added release notes --- fastlane/metadata/default/release_notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 098fd1666f..a380910cf1 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1 +1,2 @@ +- Videos in Duck Player now open in a new tab by default, making it easier to navigate between YouTube and Duck Player. This setting can also be turned off in Settings > Duck Player. - Bug fixes and other improvements. \ No newline at end of file From b1cb778da3e922f36632725018c5c45b8e1734ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 5 Nov 2024 13:42:25 +0100 Subject: [PATCH 04/18] Remove NewTabPage retain cycles (#3532) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208686031091434/f Tech Design URL: CC: **Description**: Leak 1: `NewTabPageViewController` was not dismissed properly, this caused it to stay in memory as a child view controller of `MainViewController`. In effect it was possible to dismiss FaviconFetcherTutorial. Removing the leak required to do additional changes to make it work. I moved it to `MainViewController`, so that it's not dependent on NewTabPage. Leak 2: `NewTabPageSettingsModel` was leaking via strong reference present in `NTPSettingItem`. **Steps to test this PR**: 1. Set up Sync. 2. Add favorite. 3. On another synced device open New Tab Page 4. Favicon Tutorial should appear, see if buttons work, dismissing the tutorial 5. Open and close New Tab Page a few times. 6. Open Memory Graph Debugger, verify only single instance is present for `NewTabPageViewController` and `NewTabPageSettingsModel*` objects. **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) --- DuckDuckGo/MainViewController.swift | 7 +++++-- DuckDuckGo/NewTabPageControllerDelegate.swift | 1 + DuckDuckGo/NewTabPageSettingsModel.swift | 14 +++++++------- DuckDuckGo/NewTabPageViewController.swift | 16 ++++++---------- .../NewTabPageControllerDaxDialogTests.swift | 11 +---------- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index d1a2fd2e36..14a110450f 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -128,6 +128,7 @@ class MainViewController: UIViewController { private lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger private lazy var faviconLoader: FavoritesFaviconLoading = FavoritesFaviconLoader() + private lazy var faviconsFetcherOnboarding = FaviconsFetcherOnboarding(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter) lazy var menuBookmarksViewModel: MenuBookmarksInteracting = { let viewModel = MenuBookmarksViewModel(bookmarksDatabase: bookmarksDatabase, syncService: syncService) @@ -795,8 +796,6 @@ class MainViewController: UIViewController { let controller = NewTabPageViewController(tab: tabModel, isNewTabPageCustomizationEnabled: homeTabManager.isNewTabPageSectionsEnabled, interactionModel: favoritesViewModel, - syncService: syncService, - syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, homePageMessagesConfiguration: homePageConfiguration, privacyProDataReporting: privacyProDataReporter, variantManager: variantManager, @@ -2172,6 +2171,10 @@ extension MainViewController: NewTabPageControllerDelegate { func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) { // no-op for now } + + func newTabPageDidRequestFaviconsFetcherOnboarding(_ controller: NewTabPageViewController) { + faviconsFetcherOnboarding.presentOnboardingIfNeeded(from: self) + } } extension MainViewController: NewTabPageControllerShortcutsDelegate { diff --git a/DuckDuckGo/NewTabPageControllerDelegate.swift b/DuckDuckGo/NewTabPageControllerDelegate.swift index 08ecc46c65..d36758dc58 100644 --- a/DuckDuckGo/NewTabPageControllerDelegate.swift +++ b/DuckDuckGo/NewTabPageControllerDelegate.swift @@ -24,6 +24,7 @@ protocol NewTabPageControllerDelegate: AnyObject { func newTabPageDidOpenFavoriteURL(_ controller: NewTabPageViewController, url: URL) func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) func newTabPageDidEditFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) + func newTabPageDidRequestFaviconsFetcherOnboarding(_ controller: NewTabPageViewController) } protocol NewTabPageControllerShortcutsDelegate: AnyObject { diff --git a/DuckDuckGo/NewTabPageSettingsModel.swift b/DuckDuckGo/NewTabPageSettingsModel.swift index 9c36910613..6b9fbdec6b 100644 --- a/DuckDuckGo/NewTabPageSettingsModel.swift +++ b/DuckDuckGo/NewTabPageSettingsModel.swift @@ -81,15 +81,15 @@ final class NewTabPageSettingsModel, NewTabPage { - private let syncService: DDGSyncing - private let syncBookmarksAdapter: SyncBookmarksAdapter private let variantManager: VariantManager private let newTabDialogFactory: any NewTabDaxDialogProvider private let newTabDialogTypeProvider: NewTabDialogSpecProvider - private(set) lazy var faviconsFetcherOnboarding = FaviconsFetcherOnboarding(syncService: syncService, syncBookmarksAdapter: syncBookmarksAdapter) - private let newTabPageViewModel: NewTabPageViewModel private let messagesModel: NewTabPageMessagesModel private let favoritesModel: FavoritesViewModel @@ -53,8 +49,6 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { init(tab: Tab, isNewTabPageCustomizationEnabled: Bool, interactionModel: FavoritesListInteracting, - syncService: DDGSyncing, - syncBookmarksAdapter: SyncBookmarksAdapter, homePageMessagesConfiguration: HomePageMessagesConfiguration, privacyProDataReporting: PrivacyProDataReporting? = nil, variantManager: VariantManager, @@ -63,8 +57,6 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { faviconLoader: FavoritesFaviconLoading) { self.associatedTab = tab - self.syncService = syncService - self.syncBookmarksAdapter = syncBookmarksAdapter self.variantManager = variantManager self.newTabDialogFactory = newTabDialogFactory self.newTabDialogTypeProvider = newTabDialogTypeProvider @@ -145,7 +137,8 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { private func assignFavoriteModelActions() { favoritesModel.onFaviconMissing = { [weak self] in guard let self else { return } - self.faviconsFetcherOnboarding.presentOnboardingIfNeeded(from: self) + + delegate?.newTabPageDidRequestFaviconsFetcherOnboarding(self) } favoritesModel.onFavoriteURLSelected = { [weak self] url in @@ -215,7 +208,10 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { } func dismiss() { - + delegate = nil + chromeDelegate = nil + removeFromParent() + view.removeFromSuperview() } func showNextDaxDialog() { diff --git a/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift index 440a2934df..1c6322925e 100644 --- a/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift @@ -38,14 +38,7 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { variantManager = CapturingVariantManager() dialogFactory = CapturingNewTabDaxDialogProvider() specProvider = MockNewTabDialogSpecProvider() - let dataProviders = SyncDataProviders( - bookmarksDatabase: db, - secureVaultFactory: AutofillSecureVaultFactory, - secureVaultErrorReporter: SecureVaultReporter(), - settingHandlers: [], - favoritesDisplayModeStorage: MockFavoritesDisplayModeStoring(), - syncErrorHandler: SyncErrorHandler() - ) + let remoteMessagingClient = RemoteMessagingClient( bookmarksDatabase: db, appSettings: AppSettingsMock(), @@ -60,8 +53,6 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { tab: Tab(), isNewTabPageCustomizationEnabled: false, interactionModel: MockFavoritesListInteracting(), - syncService: MockDDGSyncing(authState: .active, isSyncInProgress: false), - syncBookmarksAdapter: dataProviders.bookmarksAdapter, homePageMessagesConfiguration: homePageConfiguration, variantManager: variantManager, newTabDialogFactory: dialogFactory, From 66c516af2934ddff993cecb27b5a9c7a5b605cfa Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 15:43:37 +0100 Subject: [PATCH 05/18] Send pixel on sync secure storage read failure (#3530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201493110486074/1208686320819590/f **Description**: On investigating a hard-to-reproduce issue with sync, I noticed there's a gap in error reporting when the secure storage (keychain) is not available. This adds a pixel for that case. **Steps to test this PR**: Just a pixel in an error case. Hard to test without altering code. But if you do want to do that: 1. Enable sync 2. Change `BSK.DDGSync.SecureStorage.account()` to throw every time 3. Go to the Settings -> Sync screen 4. You should see the `sync_secure_storage_read_error` Pixel in the debug 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 | 2 ++ Core/SyncErrorHandler.swift | 2 ++ DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index f300d9febc..58518bbe63 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -624,6 +624,7 @@ extension Pixel { case syncRemoveDeviceError case syncDeleteAccountError case syncLoginExistingAccountError + case syncSecureStorageReadError case syncGetOtherDevices case syncGetOtherDevicesCopy @@ -1432,6 +1433,7 @@ extension Pixel.Event { case .syncRemoveDeviceError: return "m_d_sync_remove_device_error" 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 .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 a3ff07e794..93609732ba 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -100,6 +100,8 @@ public class SyncErrorHandler: EventMapping { Pixel.fire(pixel: .syncFailedToLoadAccount, error: error) case .failedToSetupEngine: Pixel.fire(pixel: .syncFailedToSetupEngine, error: error) + case .failedToReadSecureStore: + Pixel.fire(pixel: .syncSecureStorageReadError, 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 42d05b4f6c..a72f2588fa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10970,7 +10970,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 203.1.0; + version = 203.2.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 0be6bd842d..2a7682eb00 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" : "19f1e5c945aa92562ad2d087e8d6c99801edf656", - "version" : "203.1.0" + "revision" : "56dbee74e34d37b6e699921a0b9bce2b8f22711d", + "version" : "203.2.0" } }, { From c5a97dda39e8ab854a4631be324567cd7034b16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 5 Nov 2024 15:51:37 +0100 Subject: [PATCH 06/18] UserDefaults misbehavior monitoring (#3510) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208659072736427/f Tech Design URL: https://app.asana.com/0/481882893211075/1208618515043198/f CC: **Description**: Attempt to validate a hypothesis about unreliable/inaccessible UserDefaults data during app launch. **Steps to test this PR**: #### Statistics loader 1. Launch the app 2. Stop and put a breakpoint in `StatisticsLoader.swift:50` 3. Run the app again. On breakpoint run a debugger command: `expr statisticsStore.atb = nil` 4. Continue execution. 5. Verify proper pixel is fired. 6. On assertion go to `StatisticsLoader.load()` frame in the stack and run: `expr atbPresenceFileMarker?.unmark()` or remove the app. This will prevent assertion for next scenario. #### Ad attribution reporter 1. Enable `adAttributionReporting` feature flag. 1. Put a breakpoint in `AdAttributionPixelReporter.swift:60` 3. Run the app. On breakpoint run a debugger command: `expr attributionReportSuccessfulFileMarker?.mark()` 4. Continue execution 5. Verify proper pixel is fired. 6. On assertion go to `AdAttributionPixelReporter.reportAttributionIfNeeded()` frame in the stack and run: `expr attributionReportSuccessfulFileMarker?.unmark()` or remove the app. **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/BoolFileMarker.swift | 55 ++++++++++++++++ Core/BoolFileMarkerTests.swift | 58 ++++++++++++++++ Core/PixelEvent.swift | 10 +++ Core/StatisticsLoader.swift | 25 ++++++- Core/StorageInconsistencyMonitor.swift | 66 +++++++++++++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 12 ++++ .../AdAttributionPixelReporter.swift | 52 +++++++++++++-- DuckDuckGo/AppDelegate.swift | 1 + .../AdAttributionPixelReporterTests.swift | 23 ++++++- DuckDuckGoTests/StatisticsLoaderTests.swift | 10 ++- IntegrationTests/AtbServerTests.swift | 10 ++- 11 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 Core/BoolFileMarker.swift create mode 100644 Core/BoolFileMarkerTests.swift create mode 100644 Core/StorageInconsistencyMonitor.swift diff --git a/Core/BoolFileMarker.swift b/Core/BoolFileMarker.swift new file mode 100644 index 0000000000..d70c8f44cb --- /dev/null +++ b/Core/BoolFileMarker.swift @@ -0,0 +1,55 @@ +// +// BoolFileMarker.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. +// + +public struct BoolFileMarker { + let fileManager = FileManager.default + private let url: URL + + public var isPresent: Bool { + fileManager.fileExists(atPath: url.path) + } + + public func mark() { + if !isPresent { + fileManager.createFile(atPath: url.path, contents: nil, attributes: [.protectionKey: FileProtectionType.none]) + } + } + + public func unmark() { + if isPresent { + try? fileManager.removeItem(at: url) + } + } + + public init?(name: Name) { + guard let applicationSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { + return nil + } + + self.url = applicationSupportDirectory.appendingPathComponent(name.rawValue) + } + + public struct Name: RawRepresentable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = "\(rawValue).marker" + } + } +} diff --git a/Core/BoolFileMarkerTests.swift b/Core/BoolFileMarkerTests.swift new file mode 100644 index 0000000000..23893a5668 --- /dev/null +++ b/Core/BoolFileMarkerTests.swift @@ -0,0 +1,58 @@ +// +// BoolFileMarkerTests.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 +@testable import Core + +final class BoolFileMarkerTests: XCTestCase { + + private let marker = BoolFileMarker(name: .init(rawValue: "test"))! + + override func tearDown() { + super.tearDown() + + marker.unmark() + } + + private var testFileURL: URL? { + FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.appendingPathComponent("test.marker") + } + + func testMarkCreatesCorrectFile() throws { + + marker.mark() + + let fileURL = try XCTUnwrap(testFileURL) + + let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + XCTAssertNil(attributes[.protectionKey]) + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + XCTAssertEqual(marker.isPresent, true) + } + + func testUnmarkRemovesFile() throws { + marker.mark() + marker.unmark() + + let fileURL = try XCTUnwrap(testFileURL) + + XCTAssertFalse(marker.isPresent) + XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) + } +} diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index f300d9febc..ed52ce6a71 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -835,6 +835,11 @@ extension Pixel { // MARK: WebView Error Page Shown case webViewErrorPageShown + + // MARK: UserDefaults incositency monitoring + case protectedDataUnavailableWhenBecomeActive + case statisticsLoaderATBStateMismatch + case adAttributionReportStateMismatch } } @@ -1666,6 +1671,11 @@ extension Pixel.Event { // MARK: - DuckPlayer FE Application Telemetry case .duckPlayerLandscapeLayoutImpressions: return "duckplayer_landscape_layout_impressions" + + // MARK: UserDefaults incositency monitoring + 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" } } } diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index 38255c9097..a8001e6077 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -33,17 +33,29 @@ public class StatisticsLoader { private let returnUserMeasurement: ReturnUserMeasurement private let usageSegmentation: UsageSegmenting private let parser = AtbParser() + private let atbPresenceFileMarker = BoolFileMarker(name: .isATBPresent) + private let inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(), - usageSegmentation: UsageSegmenting = UsageSegmentation()) { + usageSegmentation: UsageSegmenting = UsageSegmentation(), + inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor()) { self.statisticsStore = statisticsStore self.returnUserMeasurement = returnUserMeasurement self.usageSegmentation = usageSegmentation + self.inconsistencyMonitoring = inconsistencyMonitoring } public func load(completion: @escaping Completion = {}) { - if statisticsStore.hasInstallStatistics { + let hasFileMarker = atbPresenceFileMarker?.isPresent ?? false + let hasInstallStatistics = statisticsStore.hasInstallStatistics + + inconsistencyMonitoring.statisticsDidLoad(hasFileMarker: hasFileMarker, hasInstallStatistics: hasInstallStatistics) + + if hasInstallStatistics { + // Synchronize file marker with current state + createATBFileMarker() + completion() return } @@ -85,10 +97,15 @@ public class StatisticsLoader { self.statisticsStore.installDate = Date() self.statisticsStore.atb = atb.version self.returnUserMeasurement.installCompletedWithATB(atb) + self.createATBFileMarker() completion() } } + private func createATBFileMarker() { + atbPresenceFileMarker?.mark() + } + public func refreshSearchRetentionAtb(completion: @escaping Completion = {}) { guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeSearchAtbURL() else { requestInstallStatistics { @@ -169,3 +186,7 @@ public class StatisticsLoader { processUsageSegmentation(atb: nil, activityType: activityType) } } + +private extension BoolFileMarker.Name { + static let isATBPresent = BoolFileMarker.Name(rawValue: "atb-present") +} diff --git a/Core/StorageInconsistencyMonitor.swift b/Core/StorageInconsistencyMonitor.swift new file mode 100644 index 0000000000..d93cc4921c --- /dev/null +++ b/Core/StorageInconsistencyMonitor.swift @@ -0,0 +1,66 @@ +// +// StorageInconsistencyMonitor.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 UIKit + +public protocol AppActivationInconsistencyMonitoring { + /// See `StorageInconsistencyMonitor` for details + func didBecomeActive(isProtectedDataAvailable: Bool) +} + +public protocol StatisticsStoreInconsistencyMonitoring { + /// See `StorageInconsistencyMonitor` for details + func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) +} + +public protocol AdAttributionReporterInconsistencyMonitoring { + /// See `StorageInconsistencyMonitor` for details + func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) +} + +/// Takes care of reporting inconsistency in storage availability and/or state. +/// See https://app.asana.com/0/481882893211075/1208618515043198/f for details. +public struct StorageInconsistencyMonitor: AppActivationInconsistencyMonitoring & StatisticsStoreInconsistencyMonitoring & AdAttributionReporterInconsistencyMonitoring { + + public init() { } + + /// Reports a pixel if data is not available while app is active + public func didBecomeActive(isProtectedDataAvailable: Bool) { + if !isProtectedDataAvailable { + Pixel.fire(pixel: .protectedDataUnavailableWhenBecomeActive) + assertionFailure("This is unexpected state, debug if possible") + } + } + + /// Reports a pixel if file marker exists but installStatistics are missing + public func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) { + if hasFileMarker == true && hasInstallStatistics == false { + Pixel.fire(pixel: .statisticsLoaderATBStateMismatch) + assertionFailure("This is unexpected state, debug if possible") + } + } + + /// Reports a pixel if file marker exists but completion flag is false + public func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) { + if hasFileMarker == true && hasCompletedFlag == false { + Pixel.fire(pixel: .adAttributionReportStateMismatch) + assertionFailure("This is unexpected state, debug if possible") + } + } +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 42d05b4f6c..bed6f3b36b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -297,12 +297,14 @@ 6F03CB052C32EFCC004179A8 /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */; }; 6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB062C32F173004179A8 /* PixelFiring.swift */; }; 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */; }; + 6F04224D2CD2A3AD00729FA6 /* StorageInconsistencyMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */; }; 6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */; }; 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */; }; 6F35379E2C4AAF2E009F8717 /* NewTabPageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379D2C4AAF2E009F8717 /* NewTabPageSettingsView.swift */; }; 6F3537A02C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */; }; 6F3537A22C4AB97A009F8717 /* NewTabPageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */; }; 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */; }; + 6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */; }; 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */; }; @@ -323,6 +325,7 @@ 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; }; 6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */; }; 6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; }; + 6F9857342CD27FA2001BE9A0 /* BoolFileMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */; }; 6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */; }; 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */; }; 6F9FFE2A2C57ADB100A238BE /* EditableShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */; }; @@ -1596,6 +1599,7 @@ 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsSectionItemView.swift; sourceTree = ""; }; 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsModel.swift; sourceTree = ""; }; 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageDaxLogoView.swift; sourceTree = ""; }; + 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolFileMarkerTests.swift; sourceTree = ""; }; 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNewTabPageView.swift; sourceTree = ""; }; @@ -1616,6 +1620,8 @@ 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = ""; }; 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorageTests.swift; sourceTree = ""; }; 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = ""; }; + 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolFileMarker.swift; sourceTree = ""; }; + 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageInconsistencyMonitor.swift; sourceTree = ""; }; 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsStorage.swift; sourceTree = ""; }; 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsStorage.swift; sourceTree = ""; }; 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableShortcutsView.swift; sourceTree = ""; }; @@ -5923,6 +5929,9 @@ F143C3191E4A99DD00CFDE3A /* Utilities */ = { isa = PBXGroup; children = ( + 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */, + 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */, + 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */, 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */, B603974829C19F6F00902A34 /* Assertions.swift */, CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */, @@ -8018,6 +8027,7 @@ 310E79BD2949CAA5007C49E8 /* FireButtonReferenceTests.swift in Sources */, 4B62C4BA25B930DD008912C6 /* AppConfigurationFetchTests.swift in Sources */, 31C7D71C27515A6300A95D0A /* MockVoiceSearchHelper.swift in Sources */, + 6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */, 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */, 98AAF8E4292EB46000DBDF06 /* BookmarksMigrationTests.swift in Sources */, 85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */, @@ -8253,6 +8263,7 @@ 9876B75E2232B36900D81D9F /* TabInstrumentation.swift in Sources */, 026DABA428242BC80089E0B5 /* MockUserAgent.swift in Sources */, 1E05D1D829C46EDA00BF9A1F /* TimedPixel.swift in Sources */, + 6F9857342CD27FA2001BE9A0 /* BoolFileMarker.swift in Sources */, C14882DC27F2011C00D59F0C /* BookmarksImporter.swift in Sources */, CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */, 37CBCA9E2A8A659C0050218F /* SyncSettingsAdapter.swift in Sources */, @@ -8328,6 +8339,7 @@ CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */, 85D2187624BF6164004373D2 /* FaviconSourcesProvider.swift in Sources */, 98B000532915C46E0034BCA0 /* LegacyBookmarksStoreMigration.swift in Sources */, + 6F04224D2CD2A3AD00729FA6 /* StorageInconsistencyMonitor.swift in Sources */, 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */, 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */, 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */, diff --git a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift index c5c5f8a3cd..227ceeca0e 100644 --- a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift +++ b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift @@ -32,16 +32,32 @@ final actor AdAttributionPixelReporter { private let pixelFiring: PixelFiringAsync.Type private var isSendingAttribution: Bool = false + private let inconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring + private let attributionReportSuccessfulFileMarker = BoolFileMarker(name: .isAttrbutionReportSuccessful) + + private var shouldReport: Bool { + get async { + if let attributionReportSuccessfulFileMarker { + // If marker is present then report only if data consistent + return await !fetcherStorage.wasAttributionReportSuccessful && !attributionReportSuccessfulFileMarker.isPresent + } else { + return await fetcherStorage.wasAttributionReportSuccessful + } + } + } + init(fetcherStorage: AdAttributionReporterStorage = UserDefaultsAdAttributionReporterStorage(), attributionFetcher: AdAttributionFetcher = DefaultAdAttributionFetcher(), featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, - pixelFiring: PixelFiringAsync.Type = Pixel.self) { + pixelFiring: PixelFiringAsync.Type = Pixel.self, + inconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring = StorageInconsistencyMonitor()) { self.fetcherStorage = fetcherStorage self.attributionFetcher = attributionFetcher - self.pixelFiring = pixelFiring self.featureFlagger = featureFlagger self.privacyConfigurationManager = privacyConfigurationManager + self.pixelFiring = pixelFiring + self.inconsistencyMonitoring = inconsistencyMonitoring } @discardableResult @@ -50,7 +66,9 @@ final actor AdAttributionPixelReporter { return false } - guard await fetcherStorage.wasAttributionReportSuccessful == false else { + await checkStorageConsistency() + + guard await shouldReport else { return false } @@ -79,7 +97,7 @@ final actor AdAttributionPixelReporter { } } - await fetcherStorage.markAttributionReportSuccessful() + await markAttributionReportSuccessful() return true } @@ -87,6 +105,28 @@ final actor AdAttributionPixelReporter { return false } + private func markAttributionReportSuccessful() async { + await fetcherStorage.markAttributionReportSuccessful() + attributionReportSuccessfulFileMarker?.mark() + } + + private func checkStorageConsistency() async { + + guard let attributionReportSuccessfulFileMarker else { return } + + let wasAttributionReportSuccessful = await fetcherStorage.wasAttributionReportSuccessful + + inconsistencyMonitoring.addAttributionReporter( + hasFileMarker: attributionReportSuccessfulFileMarker.isPresent, + hasCompletedFlag: wasAttributionReportSuccessful + ) + + // Synchronize file marker with current state (in case we have updated from previous version) + if wasAttributionReportSuccessful && !attributionReportSuccessfulFileMarker.isPresent { + attributionReportSuccessfulFileMarker.mark() + } + } + private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse, attributionToken: String?) -> [String: String] { var params: [String: String] = [:] @@ -104,6 +144,10 @@ final actor AdAttributionPixelReporter { } } +private extension BoolFileMarker.Name { + static let isAttrbutionReportSuccessful = BoolFileMarker.Name(rawValue: "ad-attribution-successful") +} + private struct AdAttributionReporterSettings { var includeToken: Bool diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index a585a0cbee..52bc2e1ac9 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -548,6 +548,7 @@ import os.log func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } + StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) syncService.initializeIfNeeded() syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift index f8846346bd..4e403893a5 100644 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift @@ -29,6 +29,8 @@ final class AdAttributionPixelReporterTests: XCTestCase { private var featureFlagger: MockFeatureFlagger! private var privacyConfigurationManager: PrivacyConfigurationManagerMock! + private let fileMarker = BoolFileMarker(name: .init(rawValue: "ad-attribution-successful"))! + override func setUpWithError() throws { attributionFetcher = AdAttributionFetcherMock() fetcherStorage = AdAttributionReporterStorageMock() @@ -36,6 +38,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { privacyConfigurationManager = PrivacyConfigurationManagerMock() featureFlagger.enabledFeatureFlags.append(.adAttributionReporting) + fileMarker.unmark() } override func tearDownWithError() throws { @@ -57,6 +60,17 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertTrue(result) } + func testDoesNotReportIfOnlyFileMarkerIsPresent() async throws { + let sut = createSUT() + fileMarker.mark() + attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + + let result = await sut.reportAttributionIfNeeded() + + XCTAssertNil(PixelFiringMock.lastPixelName) + XCTAssertFalse(result) + } + func testReportsOnce() async { let sut = createSUT() attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) @@ -211,7 +225,8 @@ final class AdAttributionPixelReporterTests: XCTestCase { attributionFetcher: attributionFetcher, featureFlagger: featureFlagger, privacyConfigurationManager: privacyConfigurationManager, - pixelFiring: PixelFiringMock.self) + pixelFiring: PixelFiringMock.self, + inconsistencyMonitoring: MockAdAttributionReporterInconsistencyMonitoring()) } } @@ -233,6 +248,12 @@ class AdAttributionFetcherMock: AdAttributionFetcher { } } +struct MockAdAttributionReporterInconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring { + func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) { + + } +} + extension AdServicesAttributionResponse { init(attribution: Bool) { self.init( diff --git a/DuckDuckGoTests/StatisticsLoaderTests.swift b/DuckDuckGoTests/StatisticsLoaderTests.swift index 64dca2b495..6855f20b6c 100644 --- a/DuckDuckGoTests/StatisticsLoaderTests.swift +++ b/DuckDuckGoTests/StatisticsLoaderTests.swift @@ -34,7 +34,9 @@ class StatisticsLoaderTests: XCTestCase { mockStatisticsStore = MockStatisticsStore() mockUsageSegmentation = MockUsageSegmentation() - testee = StatisticsLoader(statisticsStore: mockStatisticsStore, usageSegmentation: mockUsageSegmentation) + testee = StatisticsLoader(statisticsStore: mockStatisticsStore, + usageSegmentation: mockUsageSegmentation, + inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring()) } override func tearDown() { @@ -304,3 +306,9 @@ class StatisticsLoaderTests: XCTestCase { } } + +private struct MockStatisticsStoreInconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring { + func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) { + + } +} diff --git a/IntegrationTests/AtbServerTests.swift b/IntegrationTests/AtbServerTests.swift index 8d7a50a7bc..029e2bba34 100644 --- a/IntegrationTests/AtbServerTests.swift +++ b/IntegrationTests/AtbServerTests.swift @@ -34,8 +34,8 @@ class AtbServerTests: XCTestCase { super.setUp() store = MockStatisticsStore() - loader = StatisticsLoader(statisticsStore: store) - + loader = StatisticsLoader(statisticsStore: store, inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring()) + } func testExtiCall() { @@ -130,3 +130,9 @@ class MockStatisticsStore: StatisticsStore { var variant: String? } + +private struct MockStatisticsStoreInconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring { + func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) { + + } +} From 53be32e8ad05547448dd2a8717392385b18f8447 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 16:18:19 +0100 Subject: [PATCH 07/18] Release 7.144.0-1 (#3540) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a72f2588fa..21ee89d124 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9185,7 +9185,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9222,7 +9222,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9312,7 +9312,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9339,7 +9339,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9488,7 +9488,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9513,7 +9513,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9582,7 +9582,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9616,7 +9616,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9649,7 +9649,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9679,7 +9679,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9989,7 +9989,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10020,7 +10020,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10048,7 +10048,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10081,7 +10081,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10111,7 +10111,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10144,11 +10144,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10381,7 +10381,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10408,7 +10408,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10440,7 +10440,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10477,7 +10477,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10512,7 +10512,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10547,11 +10547,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10724,11 +10724,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10757,10 +10757,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 9005f0babff02e1051725ddf3683592e347f956f Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 17:21:30 +0100 Subject: [PATCH 08/18] Change save password Never for Site button to Not Now (#3471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1202926619870900/1208592991532541/f **Description**: Change the Never Save button with Not Now on mobile platforms to avoid users accidentally disabling for their top sites on first usage. Update the logic accordingly. Send dismiss pixel on press of the Never Save button. **Steps to test this PR**: 1. Clear your autofill data from the debug menu 2. Go to https://fill.dev/form/login-simple and submit some details. 3. **Make sure there is no "Never Ask for This Site" button and there is a "Not Now" button** 4. Tap the "Not Now" button. 5. Repeat step 2. 6. **Make sure the Save prompt shows again** **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) --------- Co-authored-by: amddg44 --- DuckDuckGo/SaveLoginView.swift | 10 ++++++++-- DuckDuckGo/UserText.swift | 1 + 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 | 3 +++ 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 | 8 +++++++- 27 files changed, 157 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/SaveLoginView.swift b/DuckDuckGo/SaveLoginView.swift index 9d15b185d4..2a738213d0 100644 --- a/DuckDuckGo/SaveLoginView.swift +++ b/DuckDuckGo/SaveLoginView.swift @@ -194,9 +194,15 @@ struct SaveLoginView: View { VStack(spacing: Const.Size.ctaVerticalSpacing) { AutofillViews.PrimaryButton(title: confirmButton, action: viewModel.save) + switch layoutType { + case .newUser: + AutofillViews.TertiaryButton(title: UserText.autofillSaveLoginNoThanksCTA, + action: viewModel.cancelButtonPressed) + default: + AutofillViews.TertiaryButton(title: UserText.autofillSaveLoginNeverPromptCTA, + action: viewModel.neverPrompt) + } - AutofillViews.TertiaryButton(title: UserText.autofillSaveLoginNeverPromptCTA, - action: viewModel.neverPrompt) } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 331dc45c98..15bef3837f 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -419,6 +419,7 @@ public struct UserText { public static let autofillOnboardingKeyFeaturesSecureStorageDescription = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description", value: "Passwords are encrypted, stored on device, and locked with Face ID or passcode.", comment: "Description of autofill onboarding prompt's secure storage feature") public static let autofillOnboardingKeyFeaturesSyncTitle = NSLocalizedString("autofill.onboarding.key-features.sync.title", value: "Sync between devices", comment: "Title of autofill onboarding prompt's sync feature") public static let autofillOnboardingKeyFeaturesSyncDescription = NSLocalizedString("autofill.onboarding.key-features.sync.description", value: "End-to-end encrypted and easy to set up when you’re ready.", comment: "Description of autofill onboarding prompt's sync feature") + public static let autofillSaveLoginNoThanksCTA = NSLocalizedString("autofill.save-login.no-thanks.CTA", value: "No Thanks", comment: "CTA displayed on modal asking if the user wants to dismiss the save login action for now") public static let autofillSavePasswordSaveCTA = NSLocalizedString("autofill.save-password.save.CTA", value: "Save Password", comment: "Confirm CTA displayed on modal asking for the user to save the password") public static let autofillUpdatePasswordSaveCTA = NSLocalizedString("autofill.update-password.save.CTA", value: "Update Password", comment: "Confirm CTA displayed on modal asking for the user to update the password") diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index c83ff1b255..fe5c65f295 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Търсене на пароли"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Паролите са криптирани. Никой освен Вас не може да ги види, дори ние."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Паролите са криптирани. Никой освен Вас не може да ги види, дори ние. [Научете повече](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Запазване на тази парола?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Не, благодаря"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "С DuckDuckGo Passwords & Autofill можете да съхранявате паролата по сигурен начин на устройството."; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index 129c113c73..45df7910f4 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Prohledat hesla"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Hesla jsou šifrovaná. Nikdo kromě tebe je nevidí, dokonce ani my."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Hesla jsou šifrovaná. Nikdo kromě tebe je nevidí, dokonce ani my. [Další informace](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Uložit tohle heslo?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, děkuji"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Bezpečně si ulož heslo do zařízení pomocí funkce pro ukládání a automatické vyplňování hesel DuckDuckGo."; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index 4813d438e4..382ee89485 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Søg adgangskoder"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Adgangskoderne er krypterede. Ingen andre end dig kan se dem, ikke engang os."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Adgangskoderne er krypterede. Ingen andre end dig kan se dem, ikke engang os. [Få flere oplysninger](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Gem denne adgangskode?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nej tak."; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Gem din adgangskode sikkert på enheden med DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 9c0f7ef2f1..5344dede4c 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Passwörter suchen"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Passwörter sind verschlüsselt. Niemand außer dir kann sie sehen, nicht einmal wir."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Passwörter sind verschlüsselt. Niemand außer dir kann sie sehen, nicht einmal wir. [Mehr erfahren](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Dieses Passwort speichern?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nein, danke"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Speichere dein Passwort mit DuckDuckGo Passwörter & Autovervollständigen sicher auf dem Gerät."; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 97567e39db..70c05970bd 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Αναζήτηση κωδικών πρόσβασης"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Οι κωδικοί πρόσβασης είναι κρυπτογραφημένοι. Κανείς άλλος εκτός από εσάς δεν μπορεί να τους βλέπει, ούτε καν εμείς."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Οι κωδικοί πρόσβασης είναι κρυπτογραφημένοι. Κανείς άλλος εκτός από εσάς δεν μπορεί να τους βλέπει, ούτε καν εμείς. [Μάθετε περισσότερα](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Αποθήκευση αυτού του κωδικού πρόσβασης;"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Όχι, ευχαριστώ"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Αποθηκεύστε με ασφάλεια τον κωδικό πρόσβασής σας στη συσκευή με τη λειτουργία DuckDuckGo κωδικοί πρόσβασης και αυτόματη συμπλήρωση."; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index b4b69e4eb4..2ff7e7ca21 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -593,6 +593,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Save this password?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "No Thanks"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Securely store your password on device with DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 85f5cff2a9..0b7bd2e9e0 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Buscar contraseñas"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Las contraseñas están cifradas. Nadie más que tú puede verlas, ni siquiera nosotros."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Las contraseñas están cifradas. Nadie más que tú puede verlas, ni siquiera nosotros. [Más información](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "¿Guardar esta contraseña?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "No, gracias"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Almacena de forma segura tu contraseña en el dispositivo con DuckDuckGo Contraseñas y Autocompletar."; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index f161bdab3f..38a5acaea0 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Otsi paroole"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Paroolid on krüpteeritud. Keegi peale sinu ei näe neid, isegi mitte meie."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Paroolid on krüpteeritud. Keegi peale sinu ei näe neid, isegi mitte meie. [Lisateave](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Kas salvestada see parool?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ei aitäh"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Salvesta oma parool turvaliselt seadmesse DuckDuckGo paroolide ja automaatse täitmisega."; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 9667b6d878..d0c31a4164 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Etsi salasanoja"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Salasanat salataan. Kukaan muu kuin sinä ei näe niitä, emme edes me."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Salasanat salataan. Kukaan muu kuin sinä ei näe niitä, emme edes me. [Lisätietoja](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Tallennetaanko tämä salasana?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ei kiitos"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Tallenna salasanasi turvallisesti laitteeseen DuckDuckGon salasanojen ja automaattisen täytön avulla."; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index a4797bc797..f900fa4815 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Rechercher un mot de passe"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Les mots de passe sont cryptés. Personne d'autre que vous ne peut les voir, pas même nous."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Les mots de passe sont cryptés. Personne d'autre que vous ne peut les voir, pas même nous. [En savoir plus](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Enregistrer ce mot de passe ?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Non merci"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Stockez votre mot de passe en toute sécurité sur votre appareil avec DuckDuckGo, Mots de passe et saisie automatique."; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index bbbc5ada7a..269e54b844 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Pretraživanje lozinki"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Lozinke su šifrirane. Nitko osim tebe ne može ih vidjeti, čak ni mi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Lozinke su šifrirane. Nitko osim tebe ne može ih vidjeti, čak ni mi. [Saznaj više](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Želiš li spremiti ovu lozinku?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, hvala"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Sigurno pohrani svoju lozinku na uređaj pomoću usluge automatskog popunjavanja DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 541241f5d6..52618f6a75 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Jelszavak keresése"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "A jelszavak titkosítva vannak. Rajtad kívül senki sem láthatja őket, még mi sem."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "A jelszavak titkosítva vannak. Rajtad kívül senki sem láthatja őket, még mi sem. [További információk](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Mented a jelszót?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nem, köszönöm"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Tárold biztonságosan a jelszavaidat az eszközödön a DuckDuckGo Jelszavak és automatikus kitöltés funkciójával."; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index 4cffed59af..e01a9d279f 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Cerca password"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Le password sono crittografate. Nessuno tranne te può vederle, nemmeno noi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Le password sono crittografate. Nessuno tranne te può vederle, nemmeno noi. [Scopri di più](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Salvare questa password?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "No, grazie"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Memorizza in modo sicuro la tua password sul dispositivo con DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index bad7486d4d..c116362c61 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Ieškoti slaptažodžių"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Slaptažodžiai yra užšifruoti. Niekas, išskyrus jus, negali jų matyti – net mes."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Slaptažodžiai yra užšifruoti. Niekas, išskyrus jus, negali jų matyti – net mes. [Sužinoti daugiau](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Išsaugoti šį slaptažodį?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, dėkoju"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Saugiai išsaugokite slaptažodį įrenginyje naudodami „DuckDuckGo“ slaptažodžių ir automatinio pildymo parinktį."; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 397c81bfcb..8f9c980cc1 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Meklēt paroles"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Paroles ir šifrētas. Neviens, izņemot tevi, tās nevar redzēt – pat mēs ne."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Paroles ir šifrētas. Neviens, izņemot tevi, tās nevar redzēt – pat mēs ne. [Uzzini vairāk](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Vai saglabāt šo paroli?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nē, paldies"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Droši saglabā paroli ierīcē, izmantojot DuckDuckGo paroles un automātisko aizpildīšanu."; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index 985493c5f8..cd0ded480c 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Søk i passord"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Passord krypteres. Ingen andre enn du kan se dem, ikke engang vi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Passord krypteres. Ingen andre enn du kan se dem, ikke engang vi. [Finn ut mer](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Vil du lagre dette passordet?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nei takk"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Lagre passordet ditt trygt på enheten med DuckDuckGos passord og autofyll."; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index a710d2848d..0ae46ad07e 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Wachtwoorden zoeken"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Wachtwoorden worden versleuteld. Niemand anders kan ze zien, zelfs wij niet."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Wachtwoorden worden versleuteld. Niemand anders dan jij kunt ze zien, zelfs wij niet. [Meer informatie](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Dit wachtwoord opslaan?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nee, bedankt"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Sla je wachtwoord veilig op je apparaat op met DuckDuckGo wachtwoorden en automatisch invullen."; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 183724755d..9888d7bfe0 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Wyszukaj hasła"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Hasła są szyfrowane. Nikt poza Tobą ich nie widzi, nawet my."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Hasła są szyfrowane. Nikt poza Tobą ich nie widzi, nawet my. [Więcej informacji] (DDGQuickLink: //duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Zapisać to hasło?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nie, dziękuję"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Bezpiecznie przechowuj swoje hasło na urządzeniu dzięki funkcji Hasła i autouzupełnianie DuckDuckGo."; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index d70ed96b69..7fd8ef149b 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Pesquisar palavras-passe"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "As palavras-passe estão encriptadas. Ninguém além de ti pode vê-las, nem mesmo nós."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "As palavras-passe estão encriptadas. Ninguém além de ti pode vê-las, nem mesmo nós. [Sabe mais](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Guardar esta palavra-passe?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Não, obrigado"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Guarda a tua palavra-passe com segurança no dispositivo com a funcionalidade Palavras-passe e preenchimento automático do DuckDuckGo."; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index a86f184e7b..ee562a26d3 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Caută parole"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Parolele sunt criptate. Nimeni în afară de tine nu le poate vedea, nici măcar noi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Parolele sunt criptate. Nimeni în afară de tine nu le poate vedea, nici măcar noi. [Află mai multe](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Dorești să salvezi această parolă?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nu, mulțumesc"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Stochează în siguranță parola pe dispozitiv, cu Parole și completare automată DuckDuckGo."; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 065fa8a045..5d041fb58f 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Найти пароль"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Пароли подвергаются шифрованию. Никто, кроме вас, их не увидит. Даже мы."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Пароли подвергаются шифрованию. Никто, кроме вас, их не увидит. Даже мы. [Подробнее...](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Сохранить пароль?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Нет, спасибо"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Вы можете сохранить этот пароль на устройстве под надежной защитой функции «Пароли и автозаполнение» от DuckDuckGo."; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 2f75da82d3..43c3005757 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Vyhľadávanie hesiel"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Heslá sú zašifrované. Nikto okrem vás ich nemôže vidieť, dokonca ani my."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Heslá sú zašifrované. Nikto okrem vás ich nemôže vidieť, dokonca ani my. [Viac informácií](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Uložiť toto heslo?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nie, ďakujem"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Heslo bezpečne uložte do zariadenia pomocou aplikácie DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 1d67ca1159..088ce79181 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Iskanje gesel"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Gesla so šifrirana. Nihče razen vas jih ne more videti, niti mi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Gesla so šifrirana. Nihče razen vas jih ne more videti, niti mi. [Več o tem](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Želite shraniti to geslo?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, hvala"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "S funkcijo DuckDuckGo Passwords & Autofill varno shranite geslo v napravo."; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index c847aaf2d1..de228cd093 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Sök lösenord"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Lösenorden är krypterade. Ingen annan än du kan se dem, inte ens vi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Lösenorden är krypterade. Ingen annan än du kan se dem, inte ens vi. [Läs mer](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Spara det här lösenordet?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nej tack"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Förvara ditt lösenord säkert på enheten med DuckDuckGo Lösenord och autofyll."; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index de8681a50b..5b967dc65a 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -428,7 +428,7 @@ "autofill.logins.empty-view.button.title" = "Şifreleri İçe Aktar"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle.first.paragraph" = "Kayıtlı parolaları başka bir tarayıcıdan DuckDuckGo'ya aktarabilirsiniz."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Kayıtlı parolaları başka bir tarayıcıdan DuckDuckGo'ya aktarabilirsiniz."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Henüz şifre kaydedilmedi"; @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Şifreleri ara"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Parolalar şifrelenir. Onları sizden başka kimse göremez. Biz bile."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Parolalar şifrelenir. Onları sizden başka kimse göremez. Biz bile. [Daha Fazla Bilgi](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Bu parola kaydedilsin mi?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Hayır Teşekkürler"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "DuckDuckGo Parolalar ve Otomatik Doldurma ile parolanızı cihazınızda güvenle saklayın."; From eb72d5429bdf2aca1ac737770527c8c203d10007 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 5 Nov 2024 17:39:19 +0100 Subject: [PATCH 09/18] Update C-S-S to 6.29.0 (#3541) Task/Issue URL: https://app.asana.com/0/1201048563534612/1208699974934565/f Description: This C-S-S release adds code for macOS. There are no changes to iOS. --- 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 bed6f3b36b..31a0150a02 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10982,7 +10982,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 203.1.0; + version = 203.3.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 0be6bd842d..b8f119becf 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" : "19f1e5c945aa92562ad2d087e8d6c99801edf656", - "version" : "203.1.0" + "revision" : "64a5d8d1e19951fe397305a14e521713fb0eaa49", + "version" : "203.3.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "48fee2508995d4ac02d18b3d55424adedcb4ce4f", - "version" : "6.28.0" + "revision" : "6cab7bdb584653a5dc007cc1ae827ec41c5a91bc", + "version" : "6.29.0" } }, { From b50b7fa93417d6296a8788d9222a0db1e3c7a31b Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 5 Nov 2024 17:51:26 +0100 Subject: [PATCH 10/18] Onboarding Add to Dock Refactor for Intro scenario (#3538) Task/Issue URL: https://app.asana.com/0/72649045549333/1208648960421864/f **Description**: Refactor the logic to show Add to Dock from the Onboarding Intro or the end of the contextual flow. --- Core/UserDefaultsPropertyWrapper.swift | 1 - DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/AppSettings.swift | 2 +- DuckDuckGo/AppUserDefaults.swift | 12 +- DuckDuckGo/DaxDialogs.swift | 13 ++- DuckDuckGo/MainViewController.swift | 2 +- DuckDuckGo/NewTabPageViewController.swift | 5 +- DuckDuckGo/OnboardingDebugView.swift | 52 ++++++++- .../AddToDock/AddToDockTutorialView.swift | 6 +- .../OnboardingView+AddToDockContent.swift | 96 ++++++++++++++++ .../ContextualOnboardingDialogs.swift | 17 ++- .../NewTabDaxDialogFactory.swift | 13 ++- .../ContextualDaxDialogsFactory.swift | 13 ++- .../Manager/OnboardingManager.swift | 35 ++++-- .../OnboardingIntroViewModel.swift | 20 +++- .../OnboardingIntro/OnboardingView.swift | 13 +++ DuckDuckGo/UserText.swift | 7 ++ DuckDuckGo/en.lproj/Localizable.strings | 12 ++ DuckDuckGoTests/AppSettingsMock.swift | 4 +- .../ContextualDaxDialogsFactoryTests.swift | 36 +++++- ...alOnboardingNewTabDialogFactoryTests.swift | 40 ++++++- DuckDuckGoTests/DaxDialogTests.swift | 50 ++++++++- .../OnboardingIntroViewModelTests.swift | 30 +++++ DuckDuckGoTests/OnboardingManagerMock.swift | 3 +- DuckDuckGoTests/OnboardingManagerTests.swift | 104 +++++++++++++----- .../TabViewControllerDaxDialogTests.swift | 1 + 26 files changed, 516 insertions(+), 75 deletions(-) create mode 100644 DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 0f625c97e3..1231840c9b 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -171,7 +171,6 @@ public struct UserDefaultsWrapper { // Debug keys case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled" case debugOnboardingHighlightsEnabledKey = "com.duckduckgo.ios.debug.onboardingHighlightsEnabled" - case debugOnboardingAddToDockEnabledKey = "com.duckduckgo.ios.debug.onboardingAddToDockEnabled" // Duck Player Pixel Experiment case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed.v2" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 31a0150a02..34ac5cf757 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -713,6 +713,7 @@ 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; }; + 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */; }; 9F4CC5152C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */; }; 9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */; }; 9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */; }; @@ -2514,6 +2515,7 @@ 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = ""; }; + 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddToDockContent.swift"; sourceTree = ""; }; 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterMock.swift; sourceTree = ""; }; 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerDaxDialogTests.swift; sourceTree = ""; }; 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabDelegate.swift; sourceTree = ""; }; @@ -4810,6 +4812,7 @@ 9F8E0F302CCA6390001EA7C5 /* AddToDockTutorialView.swift */, 9F8E0F372CCFAA8A001EA7C5 /* AddToDockPromoView.swift */, 9F8E0F3C2CCFD071001EA7C5 /* AddToDockPromoViewModel.swift */, + 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */, ); path = AddToDock; sourceTree = ""; @@ -7408,6 +7411,7 @@ F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, + 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index 2e212f2e59..af3f91b5a4 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -87,5 +87,5 @@ protocol AppSettings: AnyObject, AppDebugSettings { protocol AppDebugSettings { var onboardingHighlightsEnabled: Bool { get set } - var onboardingAddToDockEnabled: Bool { get set } + var onboardingAddToDockState: OnboardingAddToDockState { get set } } diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 9d1ca894a3..4cd79c3e0b 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -84,6 +84,7 @@ public class AppUserDefaults: AppSettings { private struct DebugKeys { static let inspectableWebViewsEnabledKey = "com.duckduckgo.ios.debug.inspectableWebViewsEnabled" static let autofillDebugScriptEnabledKey = "com.duckduckgo.ios.debug.autofillDebugScriptEnabled" + static let onboardingAddToDockStateKey = "com.duckduckgo.ios.debug.onboardingAddToDockState" } private var userDefaults: UserDefaults? { @@ -422,8 +423,15 @@ public class AppUserDefaults: AppSettings { @UserDefaultsWrapper(key: .debugOnboardingHighlightsEnabledKey, defaultValue: false) var onboardingHighlightsEnabled: Bool - @UserDefaultsWrapper(key: .debugOnboardingAddToDockEnabledKey, defaultValue: false) - var onboardingAddToDockEnabled: Bool + var onboardingAddToDockState: OnboardingAddToDockState { + get { + guard let rawValue = userDefaults?.string(forKey: DebugKeys.onboardingAddToDockStateKey) else { return .disabled } + return OnboardingAddToDockState(rawValue: rawValue) ?? .disabled + } + set { + userDefaults?.set(newValue.rawValue, forKey: DebugKeys.onboardingAddToDockStateKey) + } + } } extension AppUserDefaults: AppConfigurationFetchStatistics { diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index d1793efb6b..d1d30b9e3a 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -41,6 +41,7 @@ protocol ContextualOnboardingLogic { var shouldShowPrivacyButtonPulse: Bool { get } var isShowingSearchSuggestions: Bool { get } var isShowingSitesSuggestions: Bool { get } + var isShowingAddToDockDialog: Bool { get } func setSearchMessageSeen() func setFireEducationMessageSeen() @@ -211,6 +212,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { private var settings: DaxDialogsSettings private var entityProviding: EntityProviding private let variantManager: VariantManager + private let addToDockManager: OnboardingAddToDockManaging private var nextHomeScreenMessageOverride: HomeScreenSpec? @@ -222,10 +224,13 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { /// Use singleton accessor, this is only accessible for tests init(settings: DaxDialogsSettings = DefaultDaxDialogsSettings(), entityProviding: EntityProviding, - variantManager: VariantManager = DefaultVariantManager()) { + variantManager: VariantManager = DefaultVariantManager(), + onboardingManager: OnboardingAddToDockManaging = OnboardingManager() + ) { self.settings = settings self.entityProviding = entityProviding self.variantManager = variantManager + self.addToDockManager = onboardingManager } private var isNewOnboarding: Bool { @@ -276,6 +281,11 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return lastShownDaxDialogType.flatMap(BrowsingSpec.SpecType.init(rawValue:)) == .visitWebsite || currentHomeSpec == .subsequent } + var isShowingAddToDockDialog: Bool { + guard isNewOnboarding else { return false } + return currentHomeSpec == .final && addToDockManager.addToDockEnabledState == .contextual + } + var isEnabled: Bool { // skip dax dialogs in integration tests guard ProcessInfo.processInfo.environment["DAXDIALOGS"] != "false" else { return false } @@ -733,6 +743,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { private func clearOnboardingBrowsingData() { removeLastShownDaxDialog() removeLastVisitedOnboardingWebsite() + currentHomeSpec = nil } } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index d1a2fd2e36..7800b7cc2a 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -2672,7 +2672,7 @@ extension MainViewController: AutoClearWorker { // Ideally this should happen once data clearing has finished AND the animation is finished if showNextDaxDialog { self.newTabPageViewController?.showNextDaxDialog() - } else if KeyboardSettings().onNewTab { + } else if KeyboardSettings().onNewTab && !self.contextualOnboardingLogic.isShowingAddToDockDialog { // If we're showing the Add to Dock dialog prevent address bar to become first responder. We want to make sure the user focues on the Add to Dock instructions. let showKeyboardAfterFireButton = DispatchWorkItem { self.enterSearch() } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index 0f24599de6..8c02560816 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -310,9 +310,12 @@ extension NewTabPageViewController { guard let spec = dialogProvider.nextHomeScreenMessageNew() else { return } - let onDismiss = { + let onDismiss = { [weak self] in + guard let self else { return } dialogProvider.dismiss() self.dismissHostingController(didFinishNTPOnboarding: true) + // Make the address bar first responder after closing the new tab page final dialog. + self.launchNewSearch() } let daxDialogView = AnyView(factory.createDaxDialog(for: spec, onDismiss: onDismiss)) let hostingController = UIHostingController(rootView: daxDialogView) diff --git a/DuckDuckGo/OnboardingDebugView.swift b/DuckDuckGo/OnboardingDebugView.swift index 88976baa81..f6528aea25 100644 --- a/DuckDuckGo/OnboardingDebugView.swift +++ b/DuckDuckGo/OnboardingDebugView.swift @@ -22,6 +22,7 @@ import SwiftUI struct OnboardingDebugView: View { @StateObject private var viewModel = OnboardingDebugViewModel() + @State private var isShowingResetDaxDialogsAlert = false private let newOnboardingIntroStartAction: () -> Void @@ -45,8 +46,13 @@ struct OnboardingDebugView: View { } Section { - Toggle( - isOn: $viewModel.isOnboardingAddToDockLocalFlagEnabled, + Picker( + selection: $viewModel.onboardingAddToDockLocalFlagState, + content: { + ForEach(OnboardingAddToDockState.allCases) { state in + Text(verbatim: state.description).tag(state) + } + }, label: { Text(verbatim: "Onboarding Add to Dock local setting enabled") } @@ -57,6 +63,18 @@ struct OnboardingDebugView: View { Text(verbatim: "Requires internal user flag set to have an effect.") } + Section { + Button(action: { + viewModel.resetDaxDialogs() + isShowingResetDaxDialogsAlert = true + }, label: { + Text(verbatim: "Reset Dax Dialogs State") + }) + .alert(isPresented: $isShowingResetDaxDialogsAlert, content: { + Alert(title: Text(verbatim: "Dax Dialogs reset"), dismissButton: .cancel()) + }) + } + Section { Button(action: newOnboardingIntroStartAction, label: { let onboardingType = viewModel.isOnboardingHighlightsLocalFlagEnabled ? "Highlights" : "" @@ -74,22 +92,44 @@ final class OnboardingDebugViewModel: ObservableObject { } } - @Published var isOnboardingAddToDockLocalFlagEnabled: Bool { + @Published var onboardingAddToDockLocalFlagState: OnboardingAddToDockState { didSet { - manager.isAddToDockLocalFlagEnabled = isOnboardingAddToDockLocalFlagEnabled + manager.addToDockLocalFlagState = onboardingAddToDockLocalFlagState } } private let manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging + private var settings: DaxDialogsSettings - init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager()) { + init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager(), settings: DaxDialogsSettings = DefaultDaxDialogsSettings()) { self.manager = manager + self.settings = settings isOnboardingHighlightsLocalFlagEnabled = manager.isOnboardingHighlightsLocalFlagEnabled - isOnboardingAddToDockLocalFlagEnabled = manager.isAddToDockLocalFlagEnabled + onboardingAddToDockLocalFlagState = manager.addToDockLocalFlagState } + func resetDaxDialogs() { + settings.isDismissed = false + settings.homeScreenMessagesSeen = 0 + settings.browsingAfterSearchShown = false + settings.browsingWithTrackersShown = false + settings.browsingWithoutTrackersShown = false + settings.browsingMajorTrackingSiteShown = false + settings.fireMessageExperimentShown = false + settings.fireButtonPulseDateShown = nil + settings.privacyButtonPulseShown = false + settings.browsingFinalDialogShown = false + settings.lastVisitedOnboardingWebsiteURLPath = nil + settings.lastShownContextualOnboardingDialogType = nil + } } #Preview { OnboardingDebugView(onNewOnboardingIntroStartAction: {}) } + +extension OnboardingAddToDockState: Identifiable { + var id: OnboardingAddToDockState { + self + } +} diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift index e13ed55c9f..8b0a2a07bf 100644 --- a/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift @@ -32,6 +32,7 @@ struct AddToDockTutorialView: View { private let title: String private let message: String + private let cta: String private let action: () -> Void @State private var animateTitle = true @@ -44,10 +45,12 @@ struct AddToDockTutorialView: View { init( title: String, message: String, + cta: String, action: @escaping () -> Void ) { self.title = title self.message = message + self.cta = cta self.action = action } @@ -81,7 +84,7 @@ struct AddToDockTutorialView: View { } Button(action: action) { - Text(UserText.AddToDockOnboarding.Buttons.dismiss) + Text(cta) } .buttonStyle(PrimaryButtonStyle()) .visibility(showContent ? .visible : .invisible) @@ -110,6 +113,7 @@ struct AddToDockTutorial_Previews: PreviewProvider { AddToDockTutorialView( title: UserText.AddToDockOnboarding.Tutorial.title, message: UserText.AddToDockOnboarding.Tutorial.message, + cta: UserText.AddToDockOnboarding.Buttons.dismiss, action: {} ) .padding() diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift new file mode 100644 index 0000000000..8baf1f18c1 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift @@ -0,0 +1,96 @@ +// +// OnboardingView+AddToDockContent.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 SwiftUI +import Onboarding + +extension OnboardingView { + + struct AddToDockPromoContentState { + var animateTitle = true + var animateMessage = false + var showContent = false + } + + struct AddToDockPromoContent: View { + + @State private var showAddToDockTutorial = false + + private var animateTitle: Binding + private var animateMessage: Binding + private var showContent: Binding + private let dismissAction: (_ fromAddToDock: Bool) -> Void + + init( + animateTitle: Binding = .constant(true), + animateMessage: Binding = .constant(true), + showContent: Binding = .constant(false), + dismissAction: @escaping (_ fromAddToDock: Bool) -> Void + ) { + self.animateTitle = animateTitle + self.animateMessage = animateMessage + self.showContent = showContent + self.dismissAction = dismissAction + } + + var body: some View { + if showAddToDockTutorial { + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Intro.tutorialDismissCTA) { + dismissAction(true) + } + } else { + ContextualDaxDialogContent( + title: UserText.AddToDockOnboarding.Intro.title, + titleFont: Font(UIFont.daxTitle3()), + message: NSAttributedString(string: UserText.AddToDockOnboarding.Intro.message), + messageFont: Font.system(size: 16), + customView: AnyView(addToDockPromoView), + customActionView: AnyView(customActionView) + ) + } + } + + private var addToDockPromoView: some View { + AddToDockPromoView() + .aspectRatio(contentMode: .fit) + .padding(.vertical) + } + + private var customActionView: some View { + VStack { + OnboardingCTAButton( + title: UserText.AddToDockOnboarding.Buttons.addToDockTutorial, + action: { + showAddToDockTutorial = true + } + ) + + OnboardingCTAButton( + title: UserText.AddToDockOnboarding.Intro.skipCTA, + buttonStyle: .ghost, + action: { + dismissAction(false) + } + ) + } + } + + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index f5bb5d47ec..eb3aee205b 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -185,10 +185,10 @@ struct OnboardingTrackersDoneDialog: View { struct OnboardingFinalDialog: View { let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenTitle - let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton let logoPosition: DaxDialogLogoPosition let message: String + let cta: String let canShowAddToDockTutorial: Bool let dismissAction: (_ fromAddToDock: Bool) -> Void @@ -198,7 +198,7 @@ struct OnboardingFinalDialog: View { ScrollView(.vertical, showsIndicators: false) { DaxDialogView(logoPosition: logoPosition) { if showAddToDockTutorial { - OnboardingAddToDockTutorialContent { + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Buttons.dismiss) { dismissAction(true) } } else { @@ -206,6 +206,7 @@ struct OnboardingFinalDialog: View { title: title, titleFont: Font(UIFont.daxTitle3()), message: NSAttributedString(string: message), + messageFont: Font.system(size: 16), customView: AnyView(customView), customActionView: AnyView(customActionView) ) @@ -277,15 +278,17 @@ struct OnboardingCTAButton: View { struct OnboardingAddToDockTutorialContent: View { let title = UserText.AddToDockOnboarding.Tutorial.title let message = UserText.AddToDockOnboarding.Tutorial.message - let cta = UserText.AddToDockOnboarding.Buttons.dismiss + let cta: String let dismissAction: () -> Void var body: some View { AddToDockTutorialView( title: title, message: message, - action: dismissAction) + cta: cta, + action: dismissAction + ) } } @@ -322,6 +325,7 @@ struct OnboardingAddToDockTutorialContent: View { OnboardingFinalDialog( logoPosition: .top, message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, + cta: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton, canShowAddToDockTutorial: false, dismissAction: { _ in } ) @@ -332,6 +336,7 @@ struct OnboardingAddToDockTutorialContent: View { OnboardingFinalDialog( logoPosition: .left, message: UserText.AddToDockOnboarding.EndOfJourney.message, + cta: UserText.AddToDockOnboarding.Buttons.dismiss, canShowAddToDockTutorial: true, dismissAction: { _ in } ) @@ -353,11 +358,11 @@ struct OnboardingAddToDockTutorialContent: View { } #Preview("Add To Dock Tutorial - Light") { - OnboardingAddToDockTutorialContent(dismissAction: {}) + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Buttons.dismiss, dismissAction: {}) .preferredColorScheme(.light) } #Preview("Add To Dock Tutorial - Dark") { - OnboardingAddToDockTutorialContent(dismissAction: {}) + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Buttons.dismiss, dismissAction: {}) .preferredColorScheme(.dark) } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index c4f74f8217..4d1a59eb66 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -99,14 +99,19 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { } private func createFinalDialog(onDismiss: @escaping () -> Void) -> some View { - let message = if onboardingManager.isAddToDockEnabled { - UserText.AddToDockOnboarding.EndOfJourney.message + let shouldShowAddToDock = onboardingManager.addToDockEnabledState == .contextual + + let (message, cta) = if shouldShowAddToDock { + (UserText.AddToDockOnboarding.EndOfJourney.message, UserText.AddToDockOnboarding.Buttons.dismiss) } else { - onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + ( + onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, + UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton + ) } return FadeInView { - OnboardingFinalDialog(logoPosition: .top, message: message, canShowAddToDockTutorial: onboardingManager.isAddToDockEnabled) { [weak self] isDismissedFromAddToDock in + OnboardingFinalDialog(logoPosition: .top, message: message, cta: cta, canShowAddToDockTutorial: shouldShowAddToDock) { [weak self] isDismissedFromAddToDock in if isDismissedFromAddToDock { Logger.onboarding.debug("Dismissed from add to dock") } else { diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index b6b9f08289..5f0b58538c 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -182,13 +182,18 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } private func endOfJourneyDialog(delegate: ContextualOnboardingDelegate, pixelName: Pixel.Event) -> some View { - let message = if onboardingManager.isAddToDockEnabled { - UserText.AddToDockOnboarding.EndOfJourney.message + let shouldShowAddToDock = onboardingManager.addToDockEnabledState == .contextual + + let (message, cta) = if shouldShowAddToDock { + (UserText.AddToDockOnboarding.EndOfJourney.message, UserText.AddToDockOnboarding.Buttons.dismiss) } else { - onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + ( + onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, + UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton + ) } - return OnboardingFinalDialog(logoPosition: .left, message: message, canShowAddToDockTutorial: onboardingManager.isAddToDockEnabled, dismissAction: { [weak delegate, weak self] isDismissedFromAddToDock in + return OnboardingFinalDialog(logoPosition: .left, message: message, cta: cta, canShowAddToDockTutorial: shouldShowAddToDock, dismissAction: { [weak delegate, weak self] isDismissedFromAddToDock in delegate?.didTapDismissContextualOnboardingAction() if isDismissedFromAddToDock { Logger.onboarding.debug("Dismissed from add to dock") diff --git a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift index f3ab2408a4..b2f212410f 100644 --- a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift +++ b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift @@ -20,6 +20,23 @@ import BrowserServicesKit import Core +enum OnboardingAddToDockState: String, Equatable, CaseIterable, CustomStringConvertible { + case disabled + case intro + case contextual + + var description: String { + switch self { + case .disabled: + "Disabled" + case .intro: + "Onboarding Intro" + case .contextual: + "Dax Dialogs" + } + } +} + final class OnboardingManager { private var appDefaults: AppDebugSettings private let featureFlagger: FeatureFlagger @@ -75,27 +92,29 @@ extension OnboardingManager: OnboardingHighlightsManaging, OnboardingHighlightsD // MARK: - Add to Dock protocol OnboardingAddToDockManaging: AnyObject { - var isAddToDockEnabled: Bool { get } + var addToDockEnabledState: OnboardingAddToDockState { get } } protocol OnboardingAddToDockDebugging: AnyObject { - var isAddToDockLocalFlagEnabled: Bool { get set } + var addToDockLocalFlagState: OnboardingAddToDockState { get set } var isAddToDockFeatureFlagEnabled: Bool { get } } extension OnboardingManager: OnboardingAddToDockManaging, OnboardingAddToDockDebugging { - var isAddToDockEnabled: Bool { - // TODO: Add variant condition once the experiment is setup - isIphone && isAddToDockLocalFlagEnabled && isAddToDockFeatureFlagEnabled + var addToDockEnabledState: OnboardingAddToDockState { + // TODO: Add variant condition OR local conditions + guard isAddToDockFeatureFlagEnabled && isIphone else { return .disabled } + + return addToDockLocalFlagState } - var isAddToDockLocalFlagEnabled: Bool { + var addToDockLocalFlagState: OnboardingAddToDockState { get { - appDefaults.onboardingAddToDockEnabled + appDefaults.onboardingAddToDockState } set { - appDefaults.onboardingAddToDockEnabled = newValue + appDefaults.onboardingAddToDockState = newValue } } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index af1622a3d0..349a6152ee 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -31,7 +31,7 @@ final class OnboardingIntroViewModel: ObservableObject { private var introSteps: [OnboardingIntroStep] private let pixelReporter: OnboardingIntroPixelReporting - private let onboardingManager: OnboardingHighlightsManaging + private let onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging private let isIpad: Bool private let urlOpener: URLOpener private let appIconProvider: () -> AppIcon @@ -39,7 +39,7 @@ final class OnboardingIntroViewModel: ObservableObject { init( pixelReporter: OnboardingIntroPixelReporting, - onboardingManager: OnboardingHighlightsManaging = OnboardingManager(), + onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging = OnboardingManager(), isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, urlOpener: URLOpener = UIApplication.shared, appIconProvider: @escaping () -> AppIcon = { AppIconManager.shared.appIcon }, @@ -52,7 +52,9 @@ final class OnboardingIntroViewModel: ObservableObject { self.appIconProvider = appIconProvider self.addressBarPositionProvider = addressBarPositionProvider - introSteps = if onboardingManager.isOnboardingHighlightsEnabled { + introSteps = if onboardingManager.isOnboardingHighlightsEnabled && onboardingManager.addToDockEnabledState == .intro { + isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsAddToDockIphoneFlow + } else if onboardingManager.isOnboardingHighlightsEnabled { isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsIPhoneFlow } else { OnboardingIntroStep.defaultFlow @@ -85,6 +87,10 @@ final class OnboardingIntroViewModel: ObservableObject { handleSetDefaultBrowserAction() } + func addToDockContinueAction() { + state = makeViewState(for: .appIconSelection) + } + func appIconPickerContinueAction() { if appIconProvider() != .defaultAppIcon { pixelReporter.trackChooseCustomAppIconColor() @@ -130,6 +136,8 @@ private extension OnboardingIntroViewModel { OnboardingView.ViewState.onboarding(.init(type: .startOnboardingDialog, step: .hidden)) case .browserComparison: OnboardingView.ViewState.onboarding(.init(type: .browsersComparisonDialog, step: stepInfo())) + case .addToDockPromo: + OnboardingView.ViewState.onboarding(.init(type: .addToDockPromoDialog, step: stepInfo())) case .appIconSelection: OnboardingView.ViewState.onboarding(.init(type: .chooseAppIconDialog, step: stepInfo())) case .addressBarPositionSelection: @@ -140,7 +148,9 @@ private extension OnboardingIntroViewModel { } func handleSetDefaultBrowserAction() { - if onboardingManager.isOnboardingHighlightsEnabled { + if onboardingManager.addToDockEnabledState == .intro && onboardingManager.isOnboardingHighlightsEnabled { + state = makeViewState(for: .addToDockPromo) + } else if onboardingManager.isOnboardingHighlightsEnabled { state = makeViewState(for: .appIconSelection) pixelReporter.trackChooseAppIconImpression() } else { @@ -157,8 +167,10 @@ private enum OnboardingIntroStep { case browserComparison case appIconSelection case addressBarPositionSelection + case addToDockPromo static let defaultFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison] static let highlightsIPhoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] static let highlightsIPadFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection] + static let highlightsAddToDockIphoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .addToDockPromo, .appIconSelection, .addressBarPositionSelection] } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index cd834e55b2..6599129896 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -40,6 +40,7 @@ struct OnboardingView: View { @State private var appIconPickerContentState = AppIconPickerContentState() @State private var addressBarPositionContentState = AddressBarPositionContentState() + @State private var addToDockPromoContentState = AddToDockPromoContentState() init(model: OnboardingIntroViewModel) { self.model = model @@ -75,6 +76,10 @@ struct OnboardingView: View { case .browsersComparisonDialog: showComparisonButton = true animateComparisonText = false + case .addToDockPromoDialog: + addToDockPromoContentState.animateTitle = false + addToDockPromoContentState.animateMessage = false + addToDockPromoContentState.showContent = true case .chooseAppIconDialog: appIconPickerContentState.animateTitle = false appIconPickerContentState.animateMessage = false @@ -90,6 +95,8 @@ struct OnboardingView: View { introView case .browsersComparisonDialog: browsersComparisonView + case .addToDockPromoDialog: + addToDockPromoView case .chooseAppIconDialog: appIconPickerView case .chooseAddressBarPositionDialog: @@ -151,6 +158,11 @@ struct OnboardingView: View { .onboardingDaxDialogStyle() } + private var addToDockPromoView: some View { + AddToDockPromoContent(dismissAction: { _ in model.addToDockContinueAction() + }) + } + private var appIconPickerView: some View { AppIconPickerContent( animateTitle: $appIconPickerContentState.animateTitle, @@ -231,6 +243,7 @@ extension OnboardingView.ViewState.Intro { enum IntroType: Equatable { case startOnboardingDialog case browsersComparisonDialog + case addToDockPromoDialog case chooseAppIconDialog case chooseAddressBarPositionDialog } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 15bef3837f..f742067903 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1428,5 +1428,12 @@ But if you *do* want a peek under the hood, you can find more information about static let title = NSLocalizedString("contextual.onboarding.addToDock.tutorial.title", value: "Adding me to your Dock is easy.", comment: "The title of the onboarding dialog popup that explains how to add the DDG browser icon to the dock.") static let message = NSLocalizedString("contextual.onboarding.addToDock.tutorial.message", value: "Find or search for the DuckDuckGo icon on your home screen. Then press and drag into place. That’s it!", comment: "The message of the onboarding dialog popup that explains how to add the DDG browser icon to the dock.") } + + public enum Intro { + static let title = NSLocalizedString("onboarding.addToDock.title", value: "Want to add me to your Dock?", comment: "The title of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock.") + static let message = NSLocalizedString("onboarding.addToDock.message", value: "I can paddle into the Dock and perch there until you need me.", comment: "The message of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock.") + static let skipCTA = NSLocalizedString("onboarding.addToDock.cta", value: "Skip", comment: "The title of the dialog button CTA to skip adding the DDB browser icon to the dock.") + static let tutorialDismissCTA = NSLocalizedString("onboarding.addToDock.tutorial.cta", value: "Got It", comment: "Button on the Add to Dock tutorial screen of the onboarding, it will dismiss the screen and proceed to the next step.") + } } } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 2ff7e7ca21..f84a7a780f 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1859,6 +1859,18 @@ https://duckduckgo.com/mac"; /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up Hidden"; +/* The title of the dialog button CTA to skip adding the DDB browser icon to the dock. */ +"onboarding.addToDock.cta" = "Skip"; + +/* The message of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock. */ +"onboarding.addToDock.message" = "I can paddle into the Dock and perch there until you need me."; + +/* The title of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock. */ +"onboarding.addToDock.title" = "Want to add me to your Dock?"; + +/* Button on the Add to Dock tutorial screen of the onboarding, it will dismiss the screen and proceed to the next step. */ +"onboarding.addToDock.tutorial.cta" = "Got It"; + /* Button to change the default browser */ "onboarding.browsers.cta" = "Choose Your Browser"; diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index a8c3db67bf..585d936cd9 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -95,6 +95,6 @@ class AppSettingsMock: AppSettings { var newTabPageIntroMessageSeenCount: Int = 0 var onboardingHighlightsEnabled: Bool = false - var onboardingAddToDockEnabled: Bool = false - + var onboardingAddToDockState: OnboardingAddToDockState = .disabled + } diff --git a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift index 7f7665d7a6..a255eb4417 100644 --- a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift +++ b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift @@ -28,6 +28,7 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { private var delegate: ContextualOnboardingDelegateMock! private var settingsMock: ContextualOnboardingSettingsMock! private var pixelReporterMock: OnboardingPixelReporterMock! + private var onboardingManagerMock: OnboardingManagerMock! private var window: UIWindow! override func setUpWithError() throws { @@ -35,10 +36,12 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { delegate = ContextualOnboardingDelegateMock() settingsMock = ContextualOnboardingSettingsMock() pixelReporterMock = OnboardingPixelReporterMock() + onboardingManagerMock = OnboardingManagerMock() sut = ExperimentContextualDaxDialogsFactory( contextualOnboardingLogic: ContextualOnboardingLogicMock(), contextualOnboardingSettings: settingsMock, - contextualOnboardingPixelReporter: pixelReporterMock + contextualOnboardingPixelReporter: pixelReporterMock, + onboardingManager: onboardingManagerMock ) window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() @@ -50,6 +53,7 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { delegate = nil settingsMock = nil pixelReporterMock = nil + onboardingManagerMock = nil sut = nil try super.tearDownWithError() } @@ -335,6 +339,36 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { // THEN XCTAssertTrue(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) } + + // MARK: - Add To Dock + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenReturnExpectedCopy() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: delegate, onSizeUpdate: {}) + + // WHEN + let result = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // THEN + XCTAssertEqual(result.message, UserText.AddToDockOnboarding.EndOfJourney.message) + XCTAssertEqual(result.cta, UserText.AddToDockOnboarding.Buttons.dismiss) + } + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenCanShowAddToDockTutorialIsTrue() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: delegate, onSizeUpdate: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // WHEN + let result = view.canShowAddToDockTutorial + + // THEN + XCTAssertTrue(result) + } } extension ContextualDaxDialogsFactoryTests { diff --git a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift index 8d54d53d5a..aca4f086d2 100644 --- a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift +++ b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift @@ -29,6 +29,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { var mockDelegate: CapturingOnboardingNavigationDelegate! var contextualOnboardingLogicMock: ContextualOnboardingLogicMock! var pixelReporterMock: OnboardingPixelReporterMock! + var onboardingManagerMock: OnboardingManagerMock! var onDismissCalled: Bool! var window: UIWindow! @@ -36,9 +37,15 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { super.setUp() mockDelegate = CapturingOnboardingNavigationDelegate() contextualOnboardingLogicMock = ContextualOnboardingLogicMock() + onboardingManagerMock = OnboardingManagerMock() onDismissCalled = false pixelReporterMock = OnboardingPixelReporterMock() - factory = NewTabDaxDialogFactory(delegate: mockDelegate, contextualOnboardingLogic: contextualOnboardingLogicMock, onboardingPixelReporter: pixelReporterMock) + factory = NewTabDaxDialogFactory( + delegate: mockDelegate, + contextualOnboardingLogic: contextualOnboardingLogicMock, + onboardingPixelReporter: pixelReporterMock, + onboardingManager: onboardingManagerMock + ) window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() } @@ -51,6 +58,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { onDismissCalled = nil contextualOnboardingLogicMock = nil pixelReporterMock = nil + onboardingManagerMock = nil super.tearDown() } @@ -163,6 +171,36 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) } + // MARK: - Add To Dock + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenReturnExpectedCopy() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + + // WHEN + let result = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // THEN + XCTAssertEqual(result.message, UserText.AddToDockOnboarding.EndOfJourney.message) + XCTAssertEqual(result.cta, UserText.AddToDockOnboarding.Buttons.dismiss) + } + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenCanShowAddToDockTutorialIsTrue() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // WHEN + let result = view.canShowAddToDockTutorial + + // THEN + XCTAssertTrue(result) + } + } private extension ContextualOnboardingNewTabDialogFactoryTests { diff --git a/DuckDuckGoTests/DaxDialogTests.swift b/DuckDuckGoTests/DaxDialogTests.swift index 22bcfc1c7d..7a512f5cae 100644 --- a/DuckDuckGoTests/DaxDialogTests.swift +++ b/DuckDuckGoTests/DaxDialogTests.swift @@ -1100,6 +1100,52 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(result) } + // MARK: - States + + func testWhenIsShowingAddToDockDialogCalledAndHomeSpecIsFinalAndAddToDockIsEnabledThenReturnTrue() { + // GIVEN + let onboardingManagerMock = OnboardingManagerMock() + onboardingManagerMock.addToDockEnabledState = .contextual + settings.fireMessageExperimentShown = true + let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + _ = sut.nextHomeScreenMessageNew() + + // WHEN + let result = sut.isShowingAddToDockDialog + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsShowingAddToDockDialogCalledAndHomeSpecIsNotFinalThenReturnFalse() { + // GIVEN + let onboardingManagerMock = OnboardingManagerMock() + onboardingManagerMock.addToDockEnabledState = .contextual + let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + _ = sut.nextHomeScreenMessageNew() + + // WHEN + let result = sut.isShowingAddToDockDialog + + // THEN + XCTAssertFalse(result) + } + + func testWhenIsShowingAddToDockDialogCalledAndHomeSpeciIsFinalAndAddToDockIsNotEnabledReturnFalse() { + // GIVEN + let onboardingManagerMock = OnboardingManagerMock() + onboardingManagerMock.addToDockEnabledState = .disabled + settings.fireMessageExperimentShown = true + let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + _ = sut.nextHomeScreenMessageNew() + + // WHEN + let result = sut.isShowingAddToDockDialog + + // THEN + XCTAssertFalse(result) + } + private func detectedTrackerFrom(_ url: URL, pageUrl: String) -> DetectedRequest { let entity = entityProvider.entity(forHost: url.host!) return DetectedRequest(url: url.absoluteString, @@ -1123,11 +1169,11 @@ final class DaxDialog: XCTestCase { protectionStatus: protectionStatus) } - private func makeExperimentSUT(settings: DaxDialogsSettings) -> DaxDialogs { + private func makeExperimentSUT(settings: DaxDialogsSettings, onboardingManager: OnboardingAddToDockManaging = OnboardingManagerMock()) -> DaxDialogs { var mockVariantManager = MockVariantManager() mockVariantManager.isSupportedBlock = { feature in feature == .contextualDaxDialogs } - return DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) + return DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager, onboardingManager: onboardingManager) } } diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 2c8ee42d50..2637b72263 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -475,4 +475,34 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.title) } + // MARK: - Add To Dock + + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToAddToDockPromoDialogAndProgressIs2Of4() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .addToDockPromoDialog, step: .init(currentStep: 2, totalSteps: 4)))) + } + + func testWhenAddtoDockContinueActionIsCalledAndIsHighlightsIphoneFlowThenThenViewStateChangesToChooseAppIconAndProgressIs3of4() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.addToDockContinueAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 3, totalSteps: 4)))) + } + } diff --git a/DuckDuckGoTests/OnboardingManagerMock.swift b/DuckDuckGoTests/OnboardingManagerMock.swift index 9322299bfe..dcd8473b60 100644 --- a/DuckDuckGoTests/OnboardingManagerMock.swift +++ b/DuckDuckGoTests/OnboardingManagerMock.swift @@ -20,6 +20,7 @@ import Foundation @testable import DuckDuckGo -final class OnboardingManagerMock: OnboardingHighlightsManaging { +final class OnboardingManagerMock: OnboardingHighlightsManaging, OnboardingAddToDockManaging { var isOnboardingHighlightsEnabled: Bool = false + var addToDockEnabledState: OnboardingAddToDockState = .disabled } diff --git a/DuckDuckGoTests/OnboardingManagerTests.swift b/DuckDuckGoTests/OnboardingManagerTests.swift index c5f0c2bff8..ef7d174fc0 100644 --- a/DuckDuckGoTests/OnboardingManagerTests.swift +++ b/DuckDuckGoTests/OnboardingManagerTests.swift @@ -155,26 +155,37 @@ final class OnboardingManagerTests: XCTestCase { // MARK: - Add to Dock - func testWhenIsAddToDockLocalFlagEnabledCalledAndAppDefaultsOnboardingAddToDockEnabledIsTrueThenReturnTrue() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsIntroThenReturnIntro() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro // WHEN - let result = sut.isAddToDockLocalFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertTrue(result) + XCTAssertEqual(result, .intro) } - func testWhenIsAddToDockLocalFlagEnabledCalledAndAppDefaultsOnboardingAddToDockEnabledIsFalseThenReturnFalse() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsContextualThenReturnContextual() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = false + appSettingsMock.onboardingAddToDockState = .contextual // WHEN - let result = sut.isAddToDockLocalFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .contextual) + } + + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsDisabledThenReturnDisabled() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .disabled + + // WHEN + let result = sut.addToDockLocalFlagState + + // THEN + XCTAssertEqual(result, .disabled) } func testWhenIsAddToDockFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOnThenReturnTrue() { @@ -199,65 +210,102 @@ final class OnboardingManagerTests: XCTestCase { XCTAssertFalse(result) } - func testWhenIsAddToDockEnabledCalledAndLocalFlagEnabledIsFalseAndFeatureFlagIsFalseThenReturnFalse() { + func testWhenAddToDockStateCalledAndLocalFlagStateIsDisabledAndFeatureFlagIsFalseThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = false + appSettingsMock.onboardingAddToDockState = .disabled featureFlaggerMock.enabledFeatureFlags = [] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockEnabledCalledAndLocalFlagEnabledIsTrueAndFeatureFlagIsFalseThenReturnFalse() { + func testWhenAddToDockStateCalledAndLocalFlagStateIsIntroAndFeatureFlagIsFalseThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro featureFlaggerMock.enabledFeatureFlags = [] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) + } + + func testWhenAddToDockStateCalledAndLocalFlagStateIsContextualAndFeatureFlagIsFalseThenReturnDisabled() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .contextual + featureFlaggerMock.enabledFeatureFlags = [] + + // WHEN + let result = sut.addToDockEnabledState + + // THEN + XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockEnabledCalledAndLocalFlagEnabledIsFalseAndFeatureFlagEnabledIsTrueThenReturnFalse() { + func testWhenAddToDockStateCalledAndLocalFlagStateIsDisabledAndFeatureFlagEnabledIsTrueThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = false + appSettingsMock.onboardingAddToDockState = .disabled featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockEnabledAndLocalFlagEnabledIsTrueAndFeatureFlagEnabledIsTrueThenReturnTrue() { + func testWhenAddToDockStateAndLocalFlagStateIsIntroAndFeatureFlagEnabledIsTrueThenReturnIntro() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertTrue(result) + XCTAssertEqual(result, .intro) + } + + func testWhenAddToDockStateAndLocalFlagStateIsContextualAndFeatureFlagEnabledIsTrueThenReturnContextual() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .contextual + featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] + + // WHEN + let result = sut.addToDockEnabledState + + // THEN + XCTAssertEqual(result, .contextual) } - func testWhenIsAddToDockEnabledAndLocalAndFeatureFlagsAreEnabledAndDeviceIsIpadReturnFalse() { + func testWhenAddToDockStateAndLocalFlagStateIsIntroAndFeatureFlagsIsEnabledAndDeviceIsIpadReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock, isIphone: false) // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) + } + + func testWhenAddToDockStateAndLocalFlagStateIsContextualAndFeatureFlagsIsEnabledAndDeviceIsIpadReturnDisabled() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .contextual + featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock, isIphone: false) + + // WHEN + let result = sut.addToDockEnabledState + + // THEN + XCTAssertEqual(result, .disabled) } } diff --git a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift index 5bafc76247..1e22244892 100644 --- a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift @@ -242,6 +242,7 @@ final class ContextualOnboardingLogicMock: ContextualOnboardingLogic { var shouldShowPrivacyButtonPulse: Bool = false var isShowingSearchSuggestions: Bool = false var isShowingSitesSuggestions: Bool = false + var isShowingAddToDockDialog: Bool = false func setFireEducationMessageSeen() { didCallSetFireEducationMessageSeen = true From d9a02a30214a831491d60199d1e099dd5b95904f Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 18:45:09 +0100 Subject: [PATCH 11/18] Send pixel on sync secure storage failure (#3542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414235014887631/1208700858621924/f **Description**: This was reviewed here: https://github.com/duckduckgo/iOS/pull/3530 and merged already to the release branch, but there's been conflicts since. So it needs an extra PR to resolve them. **Steps to test this PR**: 1. Just make sure this compiles and is pointing to latest BSK release. **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) --------- Co-authored-by: Daniel Bernal --- Core/PixelEvent.swift | 2 ++ Core/SyncErrorHandler.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index ed52ce6a71..869d390f13 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -624,6 +624,7 @@ extension Pixel { case syncRemoveDeviceError case syncDeleteAccountError case syncLoginExistingAccountError + case syncSecureStorageReadError case syncGetOtherDevices case syncGetOtherDevicesCopy @@ -1437,6 +1438,7 @@ extension Pixel.Event { case .syncRemoveDeviceError: return "m_d_sync_remove_device_error" 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 .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 a3ff07e794..93609732ba 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -100,6 +100,8 @@ public class SyncErrorHandler: EventMapping { Pixel.fire(pixel: .syncFailedToLoadAccount, error: error) case .failedToSetupEngine: Pixel.fire(pixel: .syncFailedToSetupEngine, error: error) + case .failedToReadSecureStore: + Pixel.fire(pixel: .syncSecureStorageReadError, error: error) default: // Should this be so generic? let domainEvent = Pixel.Event.syncSentUnauthenticatedRequest From 779b5bbd8fef04a8bf4ad09657025007f788360b Mon Sep 17 00:00:00 2001 From: Shilpa Modi Date: Tue, 5 Nov 2024 11:05:02 -0800 Subject: [PATCH 12/18] Adding app backgrounded result to rule compilation (#3533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204556816597738/1208691511506504/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. 2. **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 | 1 + DuckDuckGo/RulesCompilationMonitor.swift | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 869d390f13..415cd2f1b4 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -1729,6 +1729,7 @@ extension Pixel.Event { case tabClosed = "tab_closed" case appQuit = "app_quit" + case appBackgrounded = "app_backgrounded" case success } diff --git a/DuckDuckGo/RulesCompilationMonitor.swift b/DuckDuckGo/RulesCompilationMonitor.swift index 1282bff99f..eea332470d 100644 --- a/DuckDuckGo/RulesCompilationMonitor.swift +++ b/DuckDuckGo/RulesCompilationMonitor.swift @@ -41,6 +41,11 @@ final class RulesCompilationMonitor { selector: #selector(applicationWillTerminate(_:)), name: UIApplication.willTerminateNotification, object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil) } /// Called when a Tab is going to wait for Content Blocking Rules compilation @@ -88,6 +93,18 @@ final class RulesCompilationMonitor { reportWaitTime(CACurrentMediaTime() - waitStart, result: .appQuit) } + + /// If App is going into the background while the rules are still being compiled, report the time so that we + /// do not continue to count the time in background + @objc func applicationDidEnterBackground() { + guard !didReport, + !waiters.isEmpty, + let waitStart = waitStart + else { return } + + reportWaitTime(CACurrentMediaTime() - waitStart, result: .appBackgrounded) + } + private func reportWaitTime(_ waitTime: TimeInterval, result: Pixel.Event.CompileRulesResult) { didReport = true From 680aa34f365f054cb69dcc7e5a9a6bfd668d8e9c Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 5 Nov 2024 18:30:19 -0800 Subject: [PATCH 13/18] Update BSK for PixelKit suffix change (#3534) Task/Issue URL: https://app.asana.com/0/1199230911884351/1208695427490034/f Tech Design URL: CC: Description: This PR updates BSK for the PixelKit suffix change. iOS doesn't use PixelKit yet, so it's not affected. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8c95e3ad58..4357d37e88 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 = 203.3.0; + version = 204.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 b8f119becf..be8c7990a9 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" : "64a5d8d1e19951fe397305a14e521713fb0eaa49", - "version" : "203.3.0" + "revision" : "14594b6f3f3ddbea65be2818298e2e79305d8a26", + "version" : "204.0.0" } }, { From d1ce0645dc7d8aafc73ee8126ed5ef9a09df4eb4 Mon Sep 17 00:00:00 2001 From: Lorenzo Mattei Date: Wed, 6 Nov 2024 09:46:17 +0100 Subject: [PATCH 14/18] Fix email protection test (#3539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204165176092271/1208699829406721/f Tech Design URL: CC: **Description**: Fixes the email protection test by aligning the expected copy with the new version. **Steps to test this PR**: 1. Ensure Maestro tests pass. --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- .maestro/release_tests/emailprotection.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/release_tests/emailprotection.yaml b/.maestro/release_tests/emailprotection.yaml index d976f032f3..ddbd7f49c6 100644 --- a/.maestro/release_tests/emailprotection.yaml +++ b/.maestro/release_tests/emailprotection.yaml @@ -27,7 +27,7 @@ tags: - scroll - assertVisible: Email Protection - tapOn: Email Protection -- assertVisible: Email privacy, simplified. +- assertVisible: Email privacy, protected. - assertVisible: id: searchEntry - tapOn: From cd35e1889410c11a3bf74f51688f7465d444d228 Mon Sep 17 00:00:00 2001 From: Lorenzo Mattei Date: Wed, 6 Nov 2024 10:41:58 +0100 Subject: [PATCH 15/18] Switch to free runners for tests that run on Maestro (#3546) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1203301625297703/1208705705667488/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. Ideally, check [this run](https://github.com/duckduckgo/iOS/actions/runs/11690377938) is green. Realistically, check that it's not worse than usual in any way that can be related to this change. --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- .github/workflows/end-to-end.yml | 2 +- .github/workflows/sync-end-to-end.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index 3e6b9b658b..094b1826b6 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -68,7 +68,7 @@ jobs: end-to-end-tests: name: End to end Tests needs: build-end-to-end-tests - runs-on: macos-14-xlarge + runs-on: macos-14 timeout-minutes: 90 strategy: matrix: diff --git a/.github/workflows/sync-end-to-end.yml b/.github/workflows/sync-end-to-end.yml index 5f9fdb2b87..fc7d66c5d8 100644 --- a/.github/workflows/sync-end-to-end.yml +++ b/.github/workflows/sync-end-to-end.yml @@ -68,7 +68,7 @@ jobs: sync-end-to-end-tests: name: Sync End To End Tests needs: build-for-sync-end-to-end-tests - runs-on: macos-14-xlarge + runs-on: macos-14 timeout-minutes: 90 strategy: matrix: From 08c571cc29c96fad8dcfed004f0d3e2203304b8e Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 6 Nov 2024 14:32:56 +0100 Subject: [PATCH 16/18] Onboarding Add To Dock Pixels (#3543) Task/Issue URL: https://app.asana.com/0/72649045549333/1208590665790272/f **Description**: Add pixels for "Add To Dock" experiment. --- Core/PixelEvent.swift | 16 +++++ .../OnboardingView+AddToDockContent.swift | 4 ++ .../ContextualOnboardingDialogs.swift | 4 ++ .../NewTabDaxDialogFactory.swift | 34 ++++++--- .../ContextualDaxDialogsFactory.swift | 28 ++++++-- .../OnboardingIntroViewModel.swift | 16 ++++- .../OnboardingIntro/OnboardingView.swift | 10 ++- .../Pixels/OnboardingPixelReporter.swift | 31 +++++++- .../ContextualDaxDialogsFactoryTests.swift | 68 +++++++++++++++++- ...alOnboardingNewTabDialogFactoryTests.swift | 68 +++++++++++++++++- .../OnboardingIntroViewModelTests.swift | 66 ++++++++++++++++- .../OnboardingPixelReporterMock.swift | 24 ++++++- .../OnboardingPixelReporterTests.swift | 70 +++++++++++++++++++ 13 files changed, 412 insertions(+), 27 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 415cd2f1b4..cb740707df 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -143,6 +143,8 @@ extension Pixel { case brokenSiteReport + // MARK: - Onboarding + case onboardingIntroShownUnique case onboardingIntroComparisonChartShownUnique case onboardingIntroChooseBrowserCTAPressed @@ -173,6 +175,15 @@ extension Pixel { case daxDialogsEndOfJourneyNewTabUnique case daxDialogsEndOfJourneyDismissed + // MARK: - Onboarding Add To Dock + + case onboardingAddToDockPromoImpressionsUnique + case onboardingAddToDockPromoShowTutorialCTATapped + case onboardingAddToDockPromoDismissCTATapped + case onboardingAddToDockTutorialDismissCTATapped + + // MARK: - Onboarding Add To Dock + case widgetsOnboardingCTAPressed case widgetsOnboardingDeclineOptionPressed case widgetsOnboardingMovedToBackground @@ -1004,6 +1015,11 @@ extension Pixel.Event { case .daxDialogsEndOfJourneyNewTabUnique: return "m_dx_end_new_tab_unique" case .daxDialogsEndOfJourneyDismissed: return "m_dx_end_dialog_dismissed" + case .onboardingAddToDockPromoImpressionsUnique: return "m_onboarding_add_to_dock_promo_impressions_unique" + case .onboardingAddToDockPromoShowTutorialCTATapped: return "m_onboarding_add_to_dock_promo_show_tutorial_button_tapped" + case .onboardingAddToDockPromoDismissCTATapped: return "m_onboarding_add_to_dock_promo_dismiss_button_tapped" + case .onboardingAddToDockTutorialDismissCTATapped: return "m_onboarding_add_to_dock_tutorial_dismiss_button_tapped" + case .widgetsOnboardingCTAPressed: return "m_o_w_a" case .widgetsOnboardingDeclineOptionPressed: return "m_o_w_d" case .widgetsOnboardingMovedToBackground: return "m_o_w_b" diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift index 8baf1f18c1..58056ebf2d 100644 --- a/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift @@ -35,17 +35,20 @@ extension OnboardingView { private var animateTitle: Binding private var animateMessage: Binding private var showContent: Binding + private let showTutorialAction: () -> Void private let dismissAction: (_ fromAddToDock: Bool) -> Void init( animateTitle: Binding = .constant(true), animateMessage: Binding = .constant(true), showContent: Binding = .constant(false), + showTutorialAction: @escaping () -> Void, dismissAction: @escaping (_ fromAddToDock: Bool) -> Void ) { self.animateTitle = animateTitle self.animateMessage = animateMessage self.showContent = showContent + self.showTutorialAction = showTutorialAction self.dismissAction = dismissAction } @@ -77,6 +80,7 @@ extension OnboardingView { OnboardingCTAButton( title: UserText.AddToDockOnboarding.Buttons.addToDockTutorial, action: { + showTutorialAction() showAddToDockTutorial = true } ) diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index eb3aee205b..1313920709 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -190,6 +190,7 @@ struct OnboardingFinalDialog: View { let message: String let cta: String let canShowAddToDockTutorial: Bool + let showAddToDockTutorialAction: () -> Void let dismissAction: (_ fromAddToDock: Bool) -> Void @State private var showAddToDockTutorial = false @@ -234,6 +235,7 @@ struct OnboardingFinalDialog: View { OnboardingCTAButton( title: UserText.AddToDockOnboarding.Buttons.addToDockTutorial, action: { + showAddToDockTutorialAction() showAddToDockTutorial = true } ) @@ -327,6 +329,7 @@ struct OnboardingAddToDockTutorialContent: View { message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, cta: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton, canShowAddToDockTutorial: false, + showAddToDockTutorialAction: {}, dismissAction: { _ in } ) .padding() @@ -338,6 +341,7 @@ struct OnboardingAddToDockTutorialContent: View { message: UserText.AddToDockOnboarding.EndOfJourney.message, cta: UserText.AddToDockOnboarding.Buttons.dismiss, canShowAddToDockTutorial: true, + showAddToDockTutorialAction: {}, dismissAction: { _ in } ) .padding() diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index 4d1a59eb66..e68b4db204 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -110,21 +110,39 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { ) } - return FadeInView { - OnboardingFinalDialog(logoPosition: .top, message: message, cta: cta, canShowAddToDockTutorial: shouldShowAddToDock) { [weak self] isDismissedFromAddToDock in - if isDismissedFromAddToDock { - Logger.onboarding.debug("Dismissed from add to dock") - } else { - Logger.onboarding.debug("Dismissed from end of Journey") - self?.onboardingPixelReporter.trackEndOfJourneyDialogCTAAction() + let showAddToDockTutorialAction: () -> Void = { [weak self] in + self?.onboardingPixelReporter.trackAddToDockPromoShowTutorialCTAAction() + } + + let dismissAction = { [weak self] isDismissedFromAddToDockTutorial in + if isDismissedFromAddToDockTutorial { + self?.onboardingPixelReporter.trackAddToDockTutorialDismissCTAAction() + } else { + self?.onboardingPixelReporter.trackEndOfJourneyDialogCTAAction() + if shouldShowAddToDock { + self?.onboardingPixelReporter.trackAddToDockPromoDismissCTAAction() } - onDismiss() } + onDismiss() + } + + return FadeInView { + OnboardingFinalDialog( + logoPosition: .top, + message: message, + cta: cta, + canShowAddToDockTutorial: shouldShowAddToDock, + showAddToDockTutorialAction: showAddToDockTutorialAction, + dismissAction: dismissAction + ) } .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .onFirstAppear { [weak self] in self?.contextualOnboardingLogic.setFinalOnboardingDialogSeen() self?.onboardingPixelReporter.trackScreenImpression(event: .daxDialogsEndOfJourneyNewTabUnique) + if shouldShowAddToDock { + self?.onboardingPixelReporter.trackAddToDockPromoImpression() + } } } } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index 5f0b58538c..39586c01e1 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -193,18 +193,36 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { ) } - return OnboardingFinalDialog(logoPosition: .left, message: message, cta: cta, canShowAddToDockTutorial: shouldShowAddToDock, dismissAction: { [weak delegate, weak self] isDismissedFromAddToDock in + let showAddToDockTutorialAction: () -> Void = { [weak self] in + self?.contextualOnboardingPixelReporter.trackAddToDockPromoShowTutorialCTAAction() + } + + let dismissAction = { [weak delegate, weak self] isDismissedFromAddToDockTutorial in delegate?.didTapDismissContextualOnboardingAction() - if isDismissedFromAddToDock { - Logger.onboarding.debug("Dismissed from add to dock") + if isDismissedFromAddToDockTutorial { + self?.contextualOnboardingPixelReporter.trackAddToDockTutorialDismissCTAAction() } else { - Logger.onboarding.debug("Dismissed from end of Journey") self?.contextualOnboardingPixelReporter.trackEndOfJourneyDialogCTAAction() + if shouldShowAddToDock { + self?.contextualOnboardingPixelReporter.trackAddToDockPromoDismissCTAAction() + } } - }) + } + + return OnboardingFinalDialog( + logoPosition: .left, + message: message, + cta: cta, + canShowAddToDockTutorial: shouldShowAddToDock, + showAddToDockTutorialAction: showAddToDockTutorialAction, + dismissAction: dismissAction + ) .onFirstAppear { [weak self] in self?.contextualOnboardingLogic.setFinalOnboardingDialogSeen() self?.contextualOnboardingPixelReporter.trackScreenImpression(event: pixelName) + if shouldShowAddToDock { + self?.contextualOnboardingPixelReporter.trackAddToDockPromoImpression() + } } } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index 349a6152ee..d16992322b 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -30,7 +30,7 @@ final class OnboardingIntroViewModel: ObservableObject { var onCompletingOnboardingIntro: (() -> Void)? private var introSteps: [OnboardingIntroStep] - private let pixelReporter: OnboardingIntroPixelReporting + private let pixelReporter: OnboardingIntroPixelReporting & OnboardingAddToDockReporting private let onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging private let isIpad: Bool private let urlOpener: URLOpener @@ -38,7 +38,7 @@ final class OnboardingIntroViewModel: ObservableObject { private let addressBarPositionProvider: () -> AddressBarPosition init( - pixelReporter: OnboardingIntroPixelReporting, + pixelReporter: OnboardingIntroPixelReporting & OnboardingAddToDockReporting, onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging = OnboardingManager(), isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, urlOpener: URLOpener = UIApplication.shared, @@ -87,8 +87,17 @@ final class OnboardingIntroViewModel: ObservableObject { handleSetDefaultBrowserAction() } - func addToDockContinueAction() { + func addToDockContinueAction(isShowingAddToDockTutorial: Bool) { state = makeViewState(for: .appIconSelection) + if isShowingAddToDockTutorial { + pixelReporter.trackAddToDockTutorialDismissCTAAction() + } else { + pixelReporter.trackAddToDockPromoDismissCTAAction() + } + } + + func addtoDockShowTutorialAction() { + pixelReporter.trackAddToDockPromoShowTutorialCTAAction() } func appIconPickerContinueAction() { @@ -150,6 +159,7 @@ private extension OnboardingIntroViewModel { func handleSetDefaultBrowserAction() { if onboardingManager.addToDockEnabledState == .intro && onboardingManager.isOnboardingHighlightsEnabled { state = makeViewState(for: .addToDockPromo) + pixelReporter.trackAddToDockPromoImpression() } else if onboardingManager.isOnboardingHighlightsEnabled { state = makeViewState(for: .appIconSelection) pixelReporter.trackChooseAppIconImpression() diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 6599129896..2d99d818be 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -159,8 +159,14 @@ struct OnboardingView: View { } private var addToDockPromoView: some View { - AddToDockPromoContent(dismissAction: { _ in model.addToDockContinueAction() - }) + AddToDockPromoContent( + showTutorialAction: { + model.addtoDockShowTutorialAction() + }, + dismissAction: { fromAddToDockTutorial in + model.addToDockContinueAction(isShowingAddToDockTutorial: fromAddToDockTutorial) + } + ) } private var appIconPickerView: some View { diff --git a/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift b/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift index 1882e2c3e7..0e24cd1f64 100644 --- a/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift +++ b/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift @@ -67,7 +67,14 @@ protocol OnboardingDaxDialogsReporting { func trackEndOfJourneyDialogCTAAction() } -typealias OnboardingPixelReporting = OnboardingIntroImpressionReporting & OnboardingIntroPixelReporting & OnboardingSearchSuggestionsPixelReporting & OnboardingSiteSuggestionsPixelReporting & OnboardingCustomInteractionPixelReporting & OnboardingDaxDialogsReporting +protocol OnboardingAddToDockReporting { + func trackAddToDockPromoImpression() + func trackAddToDockPromoShowTutorialCTAAction() + func trackAddToDockPromoDismissCTAAction() + func trackAddToDockTutorialDismissCTAAction() +} + +typealias OnboardingPixelReporting = OnboardingIntroImpressionReporting & OnboardingIntroPixelReporting & OnboardingSearchSuggestionsPixelReporting & OnboardingSiteSuggestionsPixelReporting & OnboardingCustomInteractionPixelReporting & OnboardingDaxDialogsReporting & OnboardingAddToDockReporting // MARK: - Implementation @@ -232,6 +239,28 @@ extension OnboardingPixelReporter: OnboardingDaxDialogsReporting { } +// MARK: - OnboardingPixelReporter + Add To Dock + +extension OnboardingPixelReporter: OnboardingAddToDockReporting { + + func trackAddToDockPromoImpression() { + fire(event: .onboardingAddToDockPromoImpressionsUnique, unique: true) + } + + func trackAddToDockPromoShowTutorialCTAAction() { + fire(event: .onboardingAddToDockPromoShowTutorialCTATapped, unique: false) + } + + func trackAddToDockPromoDismissCTAAction() { + fire(event: .onboardingAddToDockPromoDismissCTATapped, unique: false) + } + + func trackAddToDockTutorialDismissCTAAction() { + fire(event: .onboardingAddToDockTutorialDismissCTATapped, unique: false) + } + +} + struct EnqueuedPixel { let event: Pixel.Event let unique: Bool diff --git a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift index a255eb4417..ff0f0f8922 100644 --- a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift +++ b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift @@ -369,11 +369,76 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { // THEN XCTAssertTrue(result) } + + // MARK: - Add To Dock Pixels + + func testWhenEndOfJourneyAddToDockPromoDialogAppearForTheFirstTimeThenFireExpectedPixel() throws { + // GIVEN + onboardingManagerMock.addToDockEnabledState = .contextual + let spec = DaxDialogs.BrowsingSpec.final + // TEST + waitForDialogDefinedBy(spec: spec) { + XCTAssertTrue(self.pixelReporterMock.didCallTrackAddToDockPromoImpression) + } + } + + func testWhenEndOfJourneyAndAddToDockPromoShowTutorialButtonActionThenFireExpectedPixel() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: ContextualOnboardingDelegateMock(), onSizeUpdate: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoShowTutorialCTAAction) + + // WHEN + view.showAddToDockTutorialAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoShowTutorialCTAAction) + } + + func testWhenEndOfJourneyAndAddToDockPromoDismissButtonActionThenFireExpectedPixel() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: ContextualOnboardingDelegateMock(), onSizeUpdate: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoDismissCTAAction) + + // WHEN + view.dismissAction(false) + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoDismissCTAAction) + } + + func testWhenEndOfJourneyAndAddToDockTutorialDismissButtonActionThenFireExpectedPixel() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: ContextualOnboardingDelegateMock(), onSizeUpdate: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockTutorialDismissCTAAction) + + // WHEN + view.dismissAction(true) + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockTutorialDismissCTAAction) + } } extension ContextualDaxDialogsFactoryTests { func testDialogDefinedBy(spec: DaxDialogs.BrowsingSpec, firesEvent event: Pixel.Event) { + waitForDialogDefinedBy(spec: spec) { + // THEN + XCTAssertTrue(self.pixelReporterMock.didCallTrackScreenImpressionCalled) + XCTAssertEqual(self.pixelReporterMock.capturedScreenImpression, event) + } + } + + func waitForDialogDefinedBy(spec: DaxDialogs.BrowsingSpec, completionHandler: @escaping () -> Void) { // GIVEN let expectation = self.expectation(description: #function) XCTAssertFalse(pixelReporterMock.didCallTrackScreenImpressionCalled) @@ -388,8 +453,7 @@ extension ContextualDaxDialogsFactoryTests { // THEN waitForExpectations(timeout: 2.0) - XCTAssertTrue(pixelReporterMock.didCallTrackScreenImpressionCalled) - XCTAssertEqual(pixelReporterMock.capturedScreenImpression, event) + completionHandler() } } diff --git a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift index aca4f086d2..cdfe351660 100644 --- a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift +++ b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift @@ -201,11 +201,76 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { XCTAssertTrue(result) } + // MARK: - Add To Dock Pixels + + func testWhenEndOfJourneyAddToDockPromoDialogAppearForTheFirstTimeThenFireExpectedPixel() throws { + // GIVEN + onboardingManagerMock.addToDockEnabledState = .contextual + let spec = DaxDialogs.HomeScreenSpec.final + // TEST + waitForDialogDefinedBy(spec: spec) { + XCTAssertTrue(self.pixelReporterMock.didCallTrackAddToDockPromoImpression) + } + } + + func testWhenEndOfJourneyAndAddToDockPromoShowTutorialButtonActionThenFireExpectedPixel() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoShowTutorialCTAAction) + + // WHEN + view.showAddToDockTutorialAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoShowTutorialCTAAction) + } + + func testWhenEndOfJourneyAndAddToDockPromoDismissButtonActionThenFireExpectedPixel() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoDismissCTAAction) + + // WHEN + view.dismissAction(false) + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoDismissCTAAction) + } + + func testWhenEndOfJourneyAndAddToDockTutorialDismissButtonActionThenFireExpectedPixel() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockTutorialDismissCTAAction) + + // WHEN + view.dismissAction(true) + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockTutorialDismissCTAAction) + } + } private extension ContextualOnboardingNewTabDialogFactoryTests { func testDialogDefinedBy(spec: DaxDialogs.HomeScreenSpec, firesEvent event: Pixel.Event) { + waitForDialogDefinedBy(spec: spec) { + // THEN + XCTAssertTrue(self.pixelReporterMock.didCallTrackScreenImpressionCalled) + XCTAssertEqual(self.pixelReporterMock.capturedScreenImpression, event) + } + } + + func waitForDialogDefinedBy(spec: DaxDialogs.HomeScreenSpec, completionHandler: @escaping () -> Void) { // GIVEN let expectation = self.expectation(description: #function) XCTAssertFalse(pixelReporterMock.didCallTrackScreenImpressionCalled) @@ -220,8 +285,7 @@ private extension ContextualOnboardingNewTabDialogFactoryTests { // THEN waitForExpectations(timeout: 2.0) - XCTAssertTrue(pixelReporterMock.didCallTrackScreenImpressionCalled) - XCTAssertEqual(pixelReporterMock.capturedScreenImpression, event) + completionHandler() } } diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 2637b72263..8d4f854401 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -241,7 +241,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { // THEN XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } - // + func testWhenStartOnboardingActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true @@ -499,10 +499,72 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .landing) // WHEN - sut.addToDockContinueAction() + sut.addToDockContinueAction(isShowingAddToDockTutorial: false) // THEN XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 3, totalSteps: 4)))) } + // MARK: - Pixel Add To Dock + + func testWhenStateChangesToAddToDockPromoThenPixelReporterTrackAddToDockPromoImpression() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoImpression) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoImpression) + } + + func testWhenAddToDockShowTutorialActionIsCalledThenPixelReporterTrackAddToDockPromoShowTutorialCTA() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoShowTutorialCTAAction) + + // WHEN + sut.addtoDockShowTutorialAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoShowTutorialCTAAction) + } + + func testWhenAddToDockContinueActionIsCalledAndIsShowingFromAddToDockTutorialIsTrueThenPixelReporterTrackAddToDockTutorialDismissCTA() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockTutorialDismissCTAAction) + + // WHEN + sut.addToDockContinueAction(isShowingAddToDockTutorial: true) + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockTutorialDismissCTAAction) + } + + func testWhenAddToDockContinueActionIsCalledAndIsShowingFromAddToDockTutorialIsFalseThenPixelReporterTrackAddToDockTutorialDismissCTA() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackAddToDockPromoDismissCTAAction) + + // WHEN + sut.addToDockContinueAction(isShowingAddToDockTutorial: false) + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddToDockPromoDismissCTAAction) + } + } diff --git a/DuckDuckGoTests/OnboardingPixelReporterMock.swift b/DuckDuckGoTests/OnboardingPixelReporterMock.swift index 4a4b20a4a5..57a3dc059b 100644 --- a/DuckDuckGoTests/OnboardingPixelReporterMock.swift +++ b/DuckDuckGoTests/OnboardingPixelReporterMock.swift @@ -22,8 +22,7 @@ import Core import Onboarding @testable import DuckDuckGo -final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, OnboardingSiteSuggestionsPixelReporting, OnboardingSearchSuggestionsPixelReporting, OnboardingCustomInteractionPixelReporting, OnboardingDaxDialogsReporting { - +final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, OnboardingSiteSuggestionsPixelReporting, OnboardingSearchSuggestionsPixelReporting, OnboardingCustomInteractionPixelReporting, OnboardingDaxDialogsReporting, OnboardingAddToDockReporting { private(set) var didCallTrackOnboardingIntroImpression = false private(set) var didCallTrackBrowserComparisonImpression = false private(set) var didCallTrackChooseBrowserCTAAction = false @@ -46,6 +45,11 @@ final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, Onboardi private(set) var didCallTrackPrivacyDashboardOpenedForFirstTime = false private(set) var didCallTrackEndOfJourneyDialogDismiss = false + private(set) var didCallTrackAddToDockPromoImpression = false + private(set) var didCallTrackAddToDockPromoShowTutorialCTAAction = false + private(set) var didCallTrackAddToDockPromoDismissCTAAction = false + private(set) var didCallTrackAddToDockTutorialDismissCTAAction = false + func trackOnboardingIntroImpression() { didCallTrackOnboardingIntroImpression = true } @@ -106,4 +110,20 @@ final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, Onboardi func trackPrivacyDashboardOpenedForFirstTime() { didCallTrackPrivacyDashboardOpenedForFirstTime = true } + + func trackAddToDockPromoImpression() { + didCallTrackAddToDockPromoImpression = true + } + + func trackAddToDockPromoShowTutorialCTAAction() { + didCallTrackAddToDockPromoShowTutorialCTAAction = true + } + + func trackAddToDockPromoDismissCTAAction() { + didCallTrackAddToDockPromoDismissCTAAction = true + } + + func trackAddToDockTutorialDismissCTAAction() { + didCallTrackAddToDockTutorialDismissCTAAction = true + } } diff --git a/DuckDuckGoTests/OnboardingPixelReporterTests.swift b/DuckDuckGoTests/OnboardingPixelReporterTests.swift index f79808e3c9..e0445ef8f8 100644 --- a/DuckDuckGoTests/OnboardingPixelReporterTests.swift +++ b/DuckDuckGoTests/OnboardingPixelReporterTests.swift @@ -401,4 +401,74 @@ final class OnboardingPixelReporterTests: XCTestCase { XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) } + // MARK: Add To Dock Experiment + + func testWhenTrackAddToDockPromoImpressionsIsCalledThenOnboardingAddToDockPromoImpressionsPixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingAddToDockPromoImpressionsUnique + XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackAddToDockPromoImpression() + + // THEN + XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + + func testWhenTrackAddToDockPromoShowTutorialCTAActionIsCalledThenOnboardingAddToDockPromoShowTutorialCTAPixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingAddToDockPromoShowTutorialCTATapped + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackAddToDockPromoShowTutorialCTAAction() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + + func testWhenTrackAddToDockPromoDismissCTAActionThenOnboardingAddToDockPromoDismissCTAPixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingAddToDockPromoDismissCTATapped + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackAddToDockPromoDismissCTAAction() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + + func testWhenTrackAddToDockTutorialDismissCTAActionIsCalledThenonboardingAddToDockTutorialDismissCTAPixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingAddToDockTutorialDismissCTATapped + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackAddToDockTutorialDismissCTAAction() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + } From 76b1f76a97ea7b580cfe3e4a0e2a56b86b6a9eee Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 6 Nov 2024 11:25:10 -0800 Subject: [PATCH 17/18] Update Ruby to 3.3.4 (#3547) Task/Issue URL: https://app.asana.com/0/414235014887631/1208702695612235/f Tech Design URL: CC: Description: This PR updates ruby-version to 3.3.4, replacing the deprecated 3.0 version of Ruby that we were using previously. --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index b0f2dcb32f..a0891f563f 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.4 +3.3.4 From f8b9fa5e9801df9a4187439f973fd259ec36e5f2 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 6 Nov 2024 12:31:39 -0800 Subject: [PATCH 18/18] Fix VPN memory pressure monitor (#3535) Task/Issue URL: https://app.asana.com/0/414235014887631/1208695924903429/f Tech Design URL: CC: Description: This PR fixes the memory pressure monitor. Previously, the memory source and queue were not being retained in any way, so they would deallocate instantly. Now, the memory source and queue are kept as properties on the packet tunnel provider. --- ...etworkProtectionPacketTunnelProvider.swift | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index fe424988e0..67e8253bfe 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -479,28 +479,33 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) } + deinit { + memoryPressureSource?.cancel() + memoryPressureSource = nil + } + + private var memoryPressureSource: DispatchSourceMemoryPressure? + private let memoryPressureQueue = DispatchQueue(label: "com.duckduckgo.mobile.ios.NetworkExtension.memoryPressure") + private func startMonitoringMemoryPressureEvents() { - let source = DispatchSource.makeMemoryPressureSource(eventMask: .all, queue: nil) - - let queue = DispatchQueue.init(label: "com.duckduckgo.mobile.ios.alpha.NetworkExtension.memoryPressure") - queue.async { - source.setEventHandler { - let event: DispatchSource.MemoryPressureEvent = source.mask - print(event) - switch event { - case DispatchSource.MemoryPressureEvent.normal: - break - case DispatchSource.MemoryPressureEvent.warning: - DailyPixel.fire(pixel: .networkProtectionMemoryWarning) - case DispatchSource.MemoryPressureEvent.critical: - DailyPixel.fire(pixel: .networkProtectionMemoryCritical) - default: - break - } + let source = DispatchSource.makeMemoryPressureSource(eventMask: .all, queue: memoryPressureQueue) + source.setEventHandler { [weak source] in + guard let source else { return } + + let event = source.data + + if event.contains(.warning) { + Logger.networkProtectionMemory.warning("Received memory pressure warning") + DailyPixel.fire(pixel: .networkProtectionMemoryWarning) + } else if event.contains(.critical) { + Logger.networkProtectionMemory.warning("Received memory pressure critical warning") + DailyPixel.fire(pixel: .networkProtectionMemoryCritical) } - source.resume() } + + self.memoryPressureSource = source + source.activate() } private func observeServerChanges() {