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..415cd2f1b4 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 @@ -835,6 +836,11 @@ extension Pixel { // MARK: WebView Error Page Shown case webViewErrorPageShown + + // MARK: UserDefaults incositency monitoring + case protectedDataUnavailableWhenBecomeActive + case statisticsLoaderATBStateMismatch + case adAttributionReportStateMismatch } } @@ -1432,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" @@ -1666,6 +1673,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" } } } @@ -1717,6 +1729,7 @@ extension Pixel.Event { case tabClosed = "tab_closed" case appQuit = "app_quit" + case appBackgrounded = "app_backgrounded" case success } 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/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/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 b4bece2c91..8c95e3ad58 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 */; }; @@ -710,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 */; }; @@ -1596,6 +1600,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 +1621,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 = ""; }; @@ -2508,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 = ""; }; @@ -4804,6 +4812,7 @@ 9F8E0F302CCA6390001EA7C5 /* AddToDockTutorialView.swift */, 9F8E0F372CCFAA8A001EA7C5 /* AddToDockPromoView.swift */, 9F8E0F3C2CCFD071001EA7C5 /* AddToDockPromoViewModel.swift */, + 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */, ); path = AddToDock; sourceTree = ""; @@ -5923,6 +5932,9 @@ F143C3191E4A99DD00CFDE3A /* Utilities */ = { isa = PBXGroup; children = ( + 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */, + 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */, + 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */, 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */, B603974829C19F6F00902A34 /* Assertions.swift */, CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */, @@ -7399,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 */, @@ -8018,6 +8031,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 +8267,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 +8343,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 */, @@ -9185,7 +9201,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 +9238,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 +9328,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 +9355,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 +9504,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 +9529,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 +9598,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 +9632,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 +9665,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 +9695,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 +10005,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 +10036,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 +10064,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 +10097,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 +10127,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 +10160,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 +10397,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 +10424,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 +10456,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 +10493,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 +10528,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 +10563,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 +10740,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 +10773,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"; @@ -10969,8 +10985,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - branch = "sam/migrate-daily-and-count-type-to-legacy-status"; - kind = branch; + kind = exactVersion; + 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 deleted file mode 100644 index 8ee956e321..0000000000 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,212 +0,0 @@ -{ - "pins" : [ - { - "identity" : "apple-toolbox", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/apple-toolbox.git", - "state" : { - "revision" : "0c13c5f056805f2d403618ccc3bfb833c303c68d", - "version" : "3.1.2" - } - }, - { - "identity" : "barebonesbrowser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/BareBonesBrowser.git", - "state" : { - "revision" : "31e5bfedc3c2ca005640c4bf2b6959d69b0e18b9", - "version" : "0.1.0" - } - }, - { - "identity" : "bloom_cpp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/bloom_cpp.git", - "state" : { - "revision" : "8076199456290b61b4544bf2f4caf296759906a0", - "version" : "3.0.0" - } - }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", - "state" : { - "branch" : "sam/migrate-daily-and-count-type-to-legacy-status", - "revision" : "1915671f92f95f7e06870852787d96da9809a93c" - } - }, - { - "identity" : "content-scope-scripts", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/content-scope-scripts", - "state" : { - "revision" : "48fee2508995d4ac02d18b3d55424adedcb4ce4f", - "version" : "6.28.0" - } - }, - { - "identity" : "designresourceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/DesignResourcesKit", - "state" : { - "revision" : "ad133f76501edcb2bfa841e33aebc0da5f92bb5c", - "version" : "3.3.0" - } - }, - { - "identity" : "duckduckgo-autofill", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", - "state" : { - "revision" : "c992041d16ec10d790e6204dce9abf9966d1363c", - "version" : "15.1.0" - } - }, - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/GRDB.swift.git", - "state" : { - "revision" : "4225b85c9a0c50544e413a1ea1e502c802b44b35", - "version" : "2.4.0" - } - }, - { - "identity" : "gzipswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/1024jp/GzipSwift.git", - "state" : { - "revision" : "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", - "version" : "6.0.1" - } - }, - { - "identity" : "ios-js-support", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/ios-js-support", - "state" : { - "revision" : "6a6789ac8104a587316c58af75539753853b50d9", - "version" : "2.0.0" - } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher.git", - "state" : { - "revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50", - "version" : "7.12.0" - } - }, - { - "identity" : "lottie-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm.git", - "state" : { - "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", - "version" : "4.4.3" - } - }, - { - "identity" : "ohhttpstubs", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", - "state" : { - "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version" : "9.1.0" - } - }, - { - "identity" : "privacy-dashboard", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/privacy-dashboard", - "state" : { - "revision" : "53fd1a0f8d91fcf475d9220f810141007300dffd", - "version" : "7.1.1" - } - }, - { - "identity" : "punycodeswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gumob/PunycodeSwift.git", - "state" : { - "revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5", - "version" : "3.0.0" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - }, - { - "identity" : "swifter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter.git", - "state" : { - "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version" : "1.5.0" - } - }, - { - "identity" : "swiftsoup", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scinfu/SwiftSoup", - "state" : { - "revision" : "028487d4a8a291b2fe1b4392b5425b6172056148", - "version" : "2.7.2" - } - }, - { - "identity" : "sync_crypto", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/sync_crypto", - "state" : { - "revision" : "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", - "version" : "0.3.0" - } - }, - { - "identity" : "trackerradarkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", - "state" : { - "revision" : "5de0a610a7927b638a5fd463a53032c9934a2c3b", - "version" : "3.0.0" - } - }, - { - "identity" : "wireguard-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/wireguard-apple", - "state" : { - "revision" : "13fd026384b1af11048451061cc1b21434990668", - "version" : "1.1.3" - } - }, - { - "identity" : "zipfoundation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/weichsel/ZIPFoundation.git", - "state" : { - "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", - "version" : "0.9.19" - } - } - ], - "version" : 2 -} 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/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..9726c00630 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 { @@ -2672,7 +2675,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/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() { @@ -310,9 +306,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/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 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..f742067903 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") @@ -1427,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/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..f84a7a780f 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."; @@ -1856,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/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."; 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/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/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, 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/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/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 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) { + + } +} 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