From 2b49550f541eefbea2d6dde1ce4df3cc78f03692 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 4 Nov 2024 14:30:53 +0100 Subject: [PATCH 01/10] Update release notes (#3529) Added release notes --- fastlane/metadata/default/release_notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 098fd1666f..a380910cf1 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1 +1,2 @@ +- Videos in Duck Player now open in a new tab by default, making it easier to navigate between YouTube and Duck Player. This setting can also be turned off in Settings > Duck Player. - Bug fixes and other improvements. \ No newline at end of file From b1cb778da3e922f36632725018c5c45b8e1734ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 5 Nov 2024 13:42:25 +0100 Subject: [PATCH 02/10] Remove NewTabPage retain cycles (#3532) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208686031091434/f Tech Design URL: CC: **Description**: Leak 1: `NewTabPageViewController` was not dismissed properly, this caused it to stay in memory as a child view controller of `MainViewController`. In effect it was possible to dismiss FaviconFetcherTutorial. Removing the leak required to do additional changes to make it work. I moved it to `MainViewController`, so that it's not dependent on NewTabPage. Leak 2: `NewTabPageSettingsModel` was leaking via strong reference present in `NTPSettingItem`. **Steps to test this PR**: 1. Set up Sync. 2. Add favorite. 3. On another synced device open New Tab Page 4. Favicon Tutorial should appear, see if buttons work, dismissing the tutorial 5. Open and close New Tab Page a few times. 6. Open Memory Graph Debugger, verify only single instance is present for `NewTabPageViewController` and `NewTabPageSettingsModel*` objects. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo/MainViewController.swift | 7 +++++-- DuckDuckGo/NewTabPageControllerDelegate.swift | 1 + DuckDuckGo/NewTabPageSettingsModel.swift | 14 +++++++------- DuckDuckGo/NewTabPageViewController.swift | 16 ++++++---------- .../NewTabPageControllerDaxDialogTests.swift | 11 +---------- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index d1a2fd2e36..14a110450f 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -128,6 +128,7 @@ class MainViewController: UIViewController { private lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger private lazy var faviconLoader: FavoritesFaviconLoading = FavoritesFaviconLoader() + private lazy var faviconsFetcherOnboarding = FaviconsFetcherOnboarding(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter) lazy var menuBookmarksViewModel: MenuBookmarksInteracting = { let viewModel = MenuBookmarksViewModel(bookmarksDatabase: bookmarksDatabase, syncService: syncService) @@ -795,8 +796,6 @@ class MainViewController: UIViewController { let controller = NewTabPageViewController(tab: tabModel, isNewTabPageCustomizationEnabled: homeTabManager.isNewTabPageSectionsEnabled, interactionModel: favoritesViewModel, - syncService: syncService, - syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, homePageMessagesConfiguration: homePageConfiguration, privacyProDataReporting: privacyProDataReporter, variantManager: variantManager, @@ -2172,6 +2171,10 @@ extension MainViewController: NewTabPageControllerDelegate { func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) { // no-op for now } + + func newTabPageDidRequestFaviconsFetcherOnboarding(_ controller: NewTabPageViewController) { + faviconsFetcherOnboarding.presentOnboardingIfNeeded(from: self) + } } extension MainViewController: NewTabPageControllerShortcutsDelegate { diff --git a/DuckDuckGo/NewTabPageControllerDelegate.swift b/DuckDuckGo/NewTabPageControllerDelegate.swift index 08ecc46c65..d36758dc58 100644 --- a/DuckDuckGo/NewTabPageControllerDelegate.swift +++ b/DuckDuckGo/NewTabPageControllerDelegate.swift @@ -24,6 +24,7 @@ protocol NewTabPageControllerDelegate: AnyObject { func newTabPageDidOpenFavoriteURL(_ controller: NewTabPageViewController, url: URL) func newTabPageDidDeleteFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) func newTabPageDidEditFavorite(_ controller: NewTabPageViewController, favorite: BookmarkEntity) + func newTabPageDidRequestFaviconsFetcherOnboarding(_ controller: NewTabPageViewController) } protocol NewTabPageControllerShortcutsDelegate: AnyObject { diff --git a/DuckDuckGo/NewTabPageSettingsModel.swift b/DuckDuckGo/NewTabPageSettingsModel.swift index 9c36910613..6b9fbdec6b 100644 --- a/DuckDuckGo/NewTabPageSettingsModel.swift +++ b/DuckDuckGo/NewTabPageSettingsModel.swift @@ -81,15 +81,15 @@ final class NewTabPageSettingsModel, NewTabPage { - private let syncService: DDGSyncing - private let syncBookmarksAdapter: SyncBookmarksAdapter private let variantManager: VariantManager private let newTabDialogFactory: any NewTabDaxDialogProvider private let newTabDialogTypeProvider: NewTabDialogSpecProvider - private(set) lazy var faviconsFetcherOnboarding = FaviconsFetcherOnboarding(syncService: syncService, syncBookmarksAdapter: syncBookmarksAdapter) - private let newTabPageViewModel: NewTabPageViewModel private let messagesModel: NewTabPageMessagesModel private let favoritesModel: FavoritesViewModel @@ -53,8 +49,6 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { init(tab: Tab, isNewTabPageCustomizationEnabled: Bool, interactionModel: FavoritesListInteracting, - syncService: DDGSyncing, - syncBookmarksAdapter: SyncBookmarksAdapter, homePageMessagesConfiguration: HomePageMessagesConfiguration, privacyProDataReporting: PrivacyProDataReporting? = nil, variantManager: VariantManager, @@ -63,8 +57,6 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { faviconLoader: FavoritesFaviconLoading) { self.associatedTab = tab - self.syncService = syncService - self.syncBookmarksAdapter = syncBookmarksAdapter self.variantManager = variantManager self.newTabDialogFactory = newTabDialogFactory self.newTabDialogTypeProvider = newTabDialogTypeProvider @@ -145,7 +137,8 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { private func assignFavoriteModelActions() { favoritesModel.onFaviconMissing = { [weak self] in guard let self else { return } - self.faviconsFetcherOnboarding.presentOnboardingIfNeeded(from: self) + + delegate?.newTabPageDidRequestFaviconsFetcherOnboarding(self) } favoritesModel.onFavoriteURLSelected = { [weak self] url in @@ -215,7 +208,10 @@ final class NewTabPageViewController: UIHostingController, NewTabPage { } func dismiss() { - + delegate = nil + chromeDelegate = nil + removeFromParent() + view.removeFromSuperview() } func showNextDaxDialog() { diff --git a/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift index 440a2934df..1c6322925e 100644 --- a/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift @@ -38,14 +38,7 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { variantManager = CapturingVariantManager() dialogFactory = CapturingNewTabDaxDialogProvider() specProvider = MockNewTabDialogSpecProvider() - let dataProviders = SyncDataProviders( - bookmarksDatabase: db, - secureVaultFactory: AutofillSecureVaultFactory, - secureVaultErrorReporter: SecureVaultReporter(), - settingHandlers: [], - favoritesDisplayModeStorage: MockFavoritesDisplayModeStoring(), - syncErrorHandler: SyncErrorHandler() - ) + let remoteMessagingClient = RemoteMessagingClient( bookmarksDatabase: db, appSettings: AppSettingsMock(), @@ -60,8 +53,6 @@ final class NewTabPageControllerDaxDialogTests: XCTestCase { tab: Tab(), isNewTabPageCustomizationEnabled: false, interactionModel: MockFavoritesListInteracting(), - syncService: MockDDGSyncing(authState: .active, isSyncInProgress: false), - syncBookmarksAdapter: dataProviders.bookmarksAdapter, homePageMessagesConfiguration: homePageConfiguration, variantManager: variantManager, newTabDialogFactory: dialogFactory, From 66c516af2934ddff993cecb27b5a9c7a5b605cfa Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 15:43:37 +0100 Subject: [PATCH 03/10] Send pixel on sync secure storage read failure (#3530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201493110486074/1208686320819590/f **Description**: On investigating a hard-to-reproduce issue with sync, I noticed there's a gap in error reporting when the secure storage (keychain) is not available. This adds a pixel for that case. **Steps to test this PR**: Just a pixel in an error case. Hard to test without altering code. But if you do want to do that: 1. Enable sync 2. Change `BSK.DDGSync.SecureStorage.account()` to throw every time 3. Go to the Settings -> Sync screen 4. You should see the `sync_secure_storage_read_error` Pixel in the debug console **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/PixelEvent.swift | 2 ++ Core/SyncErrorHandler.swift | 2 ++ DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index f300d9febc..58518bbe63 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -624,6 +624,7 @@ extension Pixel { case syncRemoveDeviceError case syncDeleteAccountError case syncLoginExistingAccountError + case syncSecureStorageReadError case syncGetOtherDevices case syncGetOtherDevicesCopy @@ -1432,6 +1433,7 @@ extension Pixel.Event { case .syncRemoveDeviceError: return "m_d_sync_remove_device_error" case .syncDeleteAccountError: return "m_d_sync_delete_account_error" case .syncLoginExistingAccountError: return "m_d_sync_login_existing_account_error" + case .syncSecureStorageReadError: return "m_d_sync_secure_storage_error" case .syncGetOtherDevices: return "sync_get_other_devices" case .syncGetOtherDevicesCopy: return "sync_get_other_devices_copy" diff --git a/Core/SyncErrorHandler.swift b/Core/SyncErrorHandler.swift index a3ff07e794..93609732ba 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -100,6 +100,8 @@ public class SyncErrorHandler: EventMapping { Pixel.fire(pixel: .syncFailedToLoadAccount, error: error) case .failedToSetupEngine: Pixel.fire(pixel: .syncFailedToSetupEngine, error: error) + case .failedToReadSecureStore: + Pixel.fire(pixel: .syncSecureStorageReadError, error: error) default: // Should this be so generic? let domainEvent = Pixel.Event.syncSentUnauthenticatedRequest diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 42d05b4f6c..a72f2588fa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10970,7 +10970,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 203.1.0; + version = 203.2.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0be6bd842d..2a7682eb00 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "19f1e5c945aa92562ad2d087e8d6c99801edf656", - "version" : "203.1.0" + "revision" : "56dbee74e34d37b6e699921a0b9bce2b8f22711d", + "version" : "203.2.0" } }, { From c5a97dda39e8ab854a4631be324567cd7034b16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 5 Nov 2024 15:51:37 +0100 Subject: [PATCH 04/10] UserDefaults misbehavior monitoring (#3510) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208659072736427/f Tech Design URL: https://app.asana.com/0/481882893211075/1208618515043198/f CC: **Description**: Attempt to validate a hypothesis about unreliable/inaccessible UserDefaults data during app launch. **Steps to test this PR**: #### Statistics loader 1. Launch the app 2. Stop and put a breakpoint in `StatisticsLoader.swift:50` 3. Run the app again. On breakpoint run a debugger command: `expr statisticsStore.atb = nil` 4. Continue execution. 5. Verify proper pixel is fired. 6. On assertion go to `StatisticsLoader.load()` frame in the stack and run: `expr atbPresenceFileMarker?.unmark()` or remove the app. This will prevent assertion for next scenario. #### Ad attribution reporter 1. Enable `adAttributionReporting` feature flag. 1. Put a breakpoint in `AdAttributionPixelReporter.swift:60` 3. Run the app. On breakpoint run a debugger command: `expr attributionReportSuccessfulFileMarker?.mark()` 4. Continue execution 5. Verify proper pixel is fired. 6. On assertion go to `AdAttributionPixelReporter.reportAttributionIfNeeded()` frame in the stack and run: `expr attributionReportSuccessfulFileMarker?.unmark()` or remove the app. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/BoolFileMarker.swift | 55 ++++++++++++++++ Core/BoolFileMarkerTests.swift | 58 ++++++++++++++++ Core/PixelEvent.swift | 10 +++ Core/StatisticsLoader.swift | 25 ++++++- Core/StorageInconsistencyMonitor.swift | 66 +++++++++++++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 12 ++++ .../AdAttributionPixelReporter.swift | 52 +++++++++++++-- DuckDuckGo/AppDelegate.swift | 1 + .../AdAttributionPixelReporterTests.swift | 23 ++++++- DuckDuckGoTests/StatisticsLoaderTests.swift | 10 ++- IntegrationTests/AtbServerTests.swift | 10 ++- 11 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 Core/BoolFileMarker.swift create mode 100644 Core/BoolFileMarkerTests.swift create mode 100644 Core/StorageInconsistencyMonitor.swift diff --git a/Core/BoolFileMarker.swift b/Core/BoolFileMarker.swift new file mode 100644 index 0000000000..d70c8f44cb --- /dev/null +++ b/Core/BoolFileMarker.swift @@ -0,0 +1,55 @@ +// +// BoolFileMarker.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +public struct BoolFileMarker { + let fileManager = FileManager.default + private let url: URL + + public var isPresent: Bool { + fileManager.fileExists(atPath: url.path) + } + + public func mark() { + if !isPresent { + fileManager.createFile(atPath: url.path, contents: nil, attributes: [.protectionKey: FileProtectionType.none]) + } + } + + public func unmark() { + if isPresent { + try? fileManager.removeItem(at: url) + } + } + + public init?(name: Name) { + guard let applicationSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { + return nil + } + + self.url = applicationSupportDirectory.appendingPathComponent(name.rawValue) + } + + public struct Name: RawRepresentable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = "\(rawValue).marker" + } + } +} diff --git a/Core/BoolFileMarkerTests.swift b/Core/BoolFileMarkerTests.swift new file mode 100644 index 0000000000..23893a5668 --- /dev/null +++ b/Core/BoolFileMarkerTests.swift @@ -0,0 +1,58 @@ +// +// BoolFileMarkerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Core + +final class BoolFileMarkerTests: XCTestCase { + + private let marker = BoolFileMarker(name: .init(rawValue: "test"))! + + override func tearDown() { + super.tearDown() + + marker.unmark() + } + + private var testFileURL: URL? { + FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.appendingPathComponent("test.marker") + } + + func testMarkCreatesCorrectFile() throws { + + marker.mark() + + let fileURL = try XCTUnwrap(testFileURL) + + let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + XCTAssertNil(attributes[.protectionKey]) + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + XCTAssertEqual(marker.isPresent, true) + } + + func testUnmarkRemovesFile() throws { + marker.mark() + marker.unmark() + + let fileURL = try XCTUnwrap(testFileURL) + + XCTAssertFalse(marker.isPresent) + XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) + } +} diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index f300d9febc..ed52ce6a71 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -835,6 +835,11 @@ extension Pixel { // MARK: WebView Error Page Shown case webViewErrorPageShown + + // MARK: UserDefaults incositency monitoring + case protectedDataUnavailableWhenBecomeActive + case statisticsLoaderATBStateMismatch + case adAttributionReportStateMismatch } } @@ -1666,6 +1671,11 @@ extension Pixel.Event { // MARK: - DuckPlayer FE Application Telemetry case .duckPlayerLandscapeLayoutImpressions: return "duckplayer_landscape_layout_impressions" + + // MARK: UserDefaults incositency monitoring + case .protectedDataUnavailableWhenBecomeActive: return "m_protected_data_unavailable_when_become_active" + case .statisticsLoaderATBStateMismatch: return "m_statistics_loader_atb_state_mismatch" + case .adAttributionReportStateMismatch: return "m_ad_attribution_report_state_mismatch" } } } diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index 38255c9097..a8001e6077 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -33,17 +33,29 @@ public class StatisticsLoader { private let returnUserMeasurement: ReturnUserMeasurement private let usageSegmentation: UsageSegmenting private let parser = AtbParser() + private let atbPresenceFileMarker = BoolFileMarker(name: .isATBPresent) + private let inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(), - usageSegmentation: UsageSegmenting = UsageSegmentation()) { + usageSegmentation: UsageSegmenting = UsageSegmentation(), + inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor()) { self.statisticsStore = statisticsStore self.returnUserMeasurement = returnUserMeasurement self.usageSegmentation = usageSegmentation + self.inconsistencyMonitoring = inconsistencyMonitoring } public func load(completion: @escaping Completion = {}) { - if statisticsStore.hasInstallStatistics { + let hasFileMarker = atbPresenceFileMarker?.isPresent ?? false + let hasInstallStatistics = statisticsStore.hasInstallStatistics + + inconsistencyMonitoring.statisticsDidLoad(hasFileMarker: hasFileMarker, hasInstallStatistics: hasInstallStatistics) + + if hasInstallStatistics { + // Synchronize file marker with current state + createATBFileMarker() + completion() return } @@ -85,10 +97,15 @@ public class StatisticsLoader { self.statisticsStore.installDate = Date() self.statisticsStore.atb = atb.version self.returnUserMeasurement.installCompletedWithATB(atb) + self.createATBFileMarker() completion() } } + private func createATBFileMarker() { + atbPresenceFileMarker?.mark() + } + public func refreshSearchRetentionAtb(completion: @escaping Completion = {}) { guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeSearchAtbURL() else { requestInstallStatistics { @@ -169,3 +186,7 @@ public class StatisticsLoader { processUsageSegmentation(atb: nil, activityType: activityType) } } + +private extension BoolFileMarker.Name { + static let isATBPresent = BoolFileMarker.Name(rawValue: "atb-present") +} diff --git a/Core/StorageInconsistencyMonitor.swift b/Core/StorageInconsistencyMonitor.swift new file mode 100644 index 0000000000..d93cc4921c --- /dev/null +++ b/Core/StorageInconsistencyMonitor.swift @@ -0,0 +1,66 @@ +// +// StorageInconsistencyMonitor.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +public protocol AppActivationInconsistencyMonitoring { + /// See `StorageInconsistencyMonitor` for details + func didBecomeActive(isProtectedDataAvailable: Bool) +} + +public protocol StatisticsStoreInconsistencyMonitoring { + /// See `StorageInconsistencyMonitor` for details + func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) +} + +public protocol AdAttributionReporterInconsistencyMonitoring { + /// See `StorageInconsistencyMonitor` for details + func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) +} + +/// Takes care of reporting inconsistency in storage availability and/or state. +/// See https://app.asana.com/0/481882893211075/1208618515043198/f for details. +public struct StorageInconsistencyMonitor: AppActivationInconsistencyMonitoring & StatisticsStoreInconsistencyMonitoring & AdAttributionReporterInconsistencyMonitoring { + + public init() { } + + /// Reports a pixel if data is not available while app is active + public func didBecomeActive(isProtectedDataAvailable: Bool) { + if !isProtectedDataAvailable { + Pixel.fire(pixel: .protectedDataUnavailableWhenBecomeActive) + assertionFailure("This is unexpected state, debug if possible") + } + } + + /// Reports a pixel if file marker exists but installStatistics are missing + public func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) { + if hasFileMarker == true && hasInstallStatistics == false { + Pixel.fire(pixel: .statisticsLoaderATBStateMismatch) + assertionFailure("This is unexpected state, debug if possible") + } + } + + /// Reports a pixel if file marker exists but completion flag is false + public func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) { + if hasFileMarker == true && hasCompletedFlag == false { + Pixel.fire(pixel: .adAttributionReportStateMismatch) + assertionFailure("This is unexpected state, debug if possible") + } + } +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 42d05b4f6c..bed6f3b36b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -297,12 +297,14 @@ 6F03CB052C32EFCC004179A8 /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */; }; 6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB062C32F173004179A8 /* PixelFiring.swift */; }; 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */; }; + 6F04224D2CD2A3AD00729FA6 /* StorageInconsistencyMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */; }; 6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */; }; 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */; }; 6F35379E2C4AAF2E009F8717 /* NewTabPageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379D2C4AAF2E009F8717 /* NewTabPageSettingsView.swift */; }; 6F3537A02C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */; }; 6F3537A22C4AB97A009F8717 /* NewTabPageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */; }; 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */; }; + 6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */; }; 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */; }; @@ -323,6 +325,7 @@ 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; }; 6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */; }; 6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; }; + 6F9857342CD27FA2001BE9A0 /* BoolFileMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */; }; 6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */; }; 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */; }; 6F9FFE2A2C57ADB100A238BE /* EditableShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */; }; @@ -1596,6 +1599,7 @@ 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsSectionItemView.swift; sourceTree = ""; }; 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsModel.swift; sourceTree = ""; }; 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageDaxLogoView.swift; sourceTree = ""; }; + 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolFileMarkerTests.swift; sourceTree = ""; }; 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNewTabPageView.swift; sourceTree = ""; }; @@ -1616,6 +1620,8 @@ 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = ""; }; 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorageTests.swift; sourceTree = ""; }; 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = ""; }; + 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolFileMarker.swift; sourceTree = ""; }; + 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageInconsistencyMonitor.swift; sourceTree = ""; }; 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsStorage.swift; sourceTree = ""; }; 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsStorage.swift; sourceTree = ""; }; 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableShortcutsView.swift; sourceTree = ""; }; @@ -5923,6 +5929,9 @@ F143C3191E4A99DD00CFDE3A /* Utilities */ = { isa = PBXGroup; children = ( + 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */, + 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */, + 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */, 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */, B603974829C19F6F00902A34 /* Assertions.swift */, CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */, @@ -8018,6 +8027,7 @@ 310E79BD2949CAA5007C49E8 /* FireButtonReferenceTests.swift in Sources */, 4B62C4BA25B930DD008912C6 /* AppConfigurationFetchTests.swift in Sources */, 31C7D71C27515A6300A95D0A /* MockVoiceSearchHelper.swift in Sources */, + 6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */, 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */, 98AAF8E4292EB46000DBDF06 /* BookmarksMigrationTests.swift in Sources */, 85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */, @@ -8253,6 +8263,7 @@ 9876B75E2232B36900D81D9F /* TabInstrumentation.swift in Sources */, 026DABA428242BC80089E0B5 /* MockUserAgent.swift in Sources */, 1E05D1D829C46EDA00BF9A1F /* TimedPixel.swift in Sources */, + 6F9857342CD27FA2001BE9A0 /* BoolFileMarker.swift in Sources */, C14882DC27F2011C00D59F0C /* BookmarksImporter.swift in Sources */, CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */, 37CBCA9E2A8A659C0050218F /* SyncSettingsAdapter.swift in Sources */, @@ -8328,6 +8339,7 @@ CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */, 85D2187624BF6164004373D2 /* FaviconSourcesProvider.swift in Sources */, 98B000532915C46E0034BCA0 /* LegacyBookmarksStoreMigration.swift in Sources */, + 6F04224D2CD2A3AD00729FA6 /* StorageInconsistencyMonitor.swift in Sources */, 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */, 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */, 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */, diff --git a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift index c5c5f8a3cd..227ceeca0e 100644 --- a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift +++ b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift @@ -32,16 +32,32 @@ final actor AdAttributionPixelReporter { private let pixelFiring: PixelFiringAsync.Type private var isSendingAttribution: Bool = false + private let inconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring + private let attributionReportSuccessfulFileMarker = BoolFileMarker(name: .isAttrbutionReportSuccessful) + + private var shouldReport: Bool { + get async { + if let attributionReportSuccessfulFileMarker { + // If marker is present then report only if data consistent + return await !fetcherStorage.wasAttributionReportSuccessful && !attributionReportSuccessfulFileMarker.isPresent + } else { + return await fetcherStorage.wasAttributionReportSuccessful + } + } + } + init(fetcherStorage: AdAttributionReporterStorage = UserDefaultsAdAttributionReporterStorage(), attributionFetcher: AdAttributionFetcher = DefaultAdAttributionFetcher(), featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, - pixelFiring: PixelFiringAsync.Type = Pixel.self) { + pixelFiring: PixelFiringAsync.Type = Pixel.self, + inconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring = StorageInconsistencyMonitor()) { self.fetcherStorage = fetcherStorage self.attributionFetcher = attributionFetcher - self.pixelFiring = pixelFiring self.featureFlagger = featureFlagger self.privacyConfigurationManager = privacyConfigurationManager + self.pixelFiring = pixelFiring + self.inconsistencyMonitoring = inconsistencyMonitoring } @discardableResult @@ -50,7 +66,9 @@ final actor AdAttributionPixelReporter { return false } - guard await fetcherStorage.wasAttributionReportSuccessful == false else { + await checkStorageConsistency() + + guard await shouldReport else { return false } @@ -79,7 +97,7 @@ final actor AdAttributionPixelReporter { } } - await fetcherStorage.markAttributionReportSuccessful() + await markAttributionReportSuccessful() return true } @@ -87,6 +105,28 @@ final actor AdAttributionPixelReporter { return false } + private func markAttributionReportSuccessful() async { + await fetcherStorage.markAttributionReportSuccessful() + attributionReportSuccessfulFileMarker?.mark() + } + + private func checkStorageConsistency() async { + + guard let attributionReportSuccessfulFileMarker else { return } + + let wasAttributionReportSuccessful = await fetcherStorage.wasAttributionReportSuccessful + + inconsistencyMonitoring.addAttributionReporter( + hasFileMarker: attributionReportSuccessfulFileMarker.isPresent, + hasCompletedFlag: wasAttributionReportSuccessful + ) + + // Synchronize file marker with current state (in case we have updated from previous version) + if wasAttributionReportSuccessful && !attributionReportSuccessfulFileMarker.isPresent { + attributionReportSuccessfulFileMarker.mark() + } + } + private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse, attributionToken: String?) -> [String: String] { var params: [String: String] = [:] @@ -104,6 +144,10 @@ final actor AdAttributionPixelReporter { } } +private extension BoolFileMarker.Name { + static let isAttrbutionReportSuccessful = BoolFileMarker.Name(rawValue: "ad-attribution-successful") +} + private struct AdAttributionReporterSettings { var includeToken: Bool diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index a585a0cbee..52bc2e1ac9 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -548,6 +548,7 @@ import os.log func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } + StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) syncService.initializeIfNeeded() syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift index f8846346bd..4e403893a5 100644 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift @@ -29,6 +29,8 @@ final class AdAttributionPixelReporterTests: XCTestCase { private var featureFlagger: MockFeatureFlagger! private var privacyConfigurationManager: PrivacyConfigurationManagerMock! + private let fileMarker = BoolFileMarker(name: .init(rawValue: "ad-attribution-successful"))! + override func setUpWithError() throws { attributionFetcher = AdAttributionFetcherMock() fetcherStorage = AdAttributionReporterStorageMock() @@ -36,6 +38,7 @@ final class AdAttributionPixelReporterTests: XCTestCase { privacyConfigurationManager = PrivacyConfigurationManagerMock() featureFlagger.enabledFeatureFlags.append(.adAttributionReporting) + fileMarker.unmark() } override func tearDownWithError() throws { @@ -57,6 +60,17 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertTrue(result) } + func testDoesNotReportIfOnlyFileMarkerIsPresent() async throws { + let sut = createSUT() + fileMarker.mark() + attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) + + let result = await sut.reportAttributionIfNeeded() + + XCTAssertNil(PixelFiringMock.lastPixelName) + XCTAssertFalse(result) + } + func testReportsOnce() async { let sut = createSUT() attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: true)) @@ -211,7 +225,8 @@ final class AdAttributionPixelReporterTests: XCTestCase { attributionFetcher: attributionFetcher, featureFlagger: featureFlagger, privacyConfigurationManager: privacyConfigurationManager, - pixelFiring: PixelFiringMock.self) + pixelFiring: PixelFiringMock.self, + inconsistencyMonitoring: MockAdAttributionReporterInconsistencyMonitoring()) } } @@ -233,6 +248,12 @@ class AdAttributionFetcherMock: AdAttributionFetcher { } } +struct MockAdAttributionReporterInconsistencyMonitoring: AdAttributionReporterInconsistencyMonitoring { + func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) { + + } +} + extension AdServicesAttributionResponse { init(attribution: Bool) { self.init( diff --git a/DuckDuckGoTests/StatisticsLoaderTests.swift b/DuckDuckGoTests/StatisticsLoaderTests.swift index 64dca2b495..6855f20b6c 100644 --- a/DuckDuckGoTests/StatisticsLoaderTests.swift +++ b/DuckDuckGoTests/StatisticsLoaderTests.swift @@ -34,7 +34,9 @@ class StatisticsLoaderTests: XCTestCase { mockStatisticsStore = MockStatisticsStore() mockUsageSegmentation = MockUsageSegmentation() - testee = StatisticsLoader(statisticsStore: mockStatisticsStore, usageSegmentation: mockUsageSegmentation) + testee = StatisticsLoader(statisticsStore: mockStatisticsStore, + usageSegmentation: mockUsageSegmentation, + inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring()) } override func tearDown() { @@ -304,3 +306,9 @@ class StatisticsLoaderTests: XCTestCase { } } + +private struct MockStatisticsStoreInconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring { + func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) { + + } +} diff --git a/IntegrationTests/AtbServerTests.swift b/IntegrationTests/AtbServerTests.swift index 8d7a50a7bc..029e2bba34 100644 --- a/IntegrationTests/AtbServerTests.swift +++ b/IntegrationTests/AtbServerTests.swift @@ -34,8 +34,8 @@ class AtbServerTests: XCTestCase { super.setUp() store = MockStatisticsStore() - loader = StatisticsLoader(statisticsStore: store) - + loader = StatisticsLoader(statisticsStore: store, inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring()) + } func testExtiCall() { @@ -130,3 +130,9 @@ class MockStatisticsStore: StatisticsStore { var variant: String? } + +private struct MockStatisticsStoreInconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring { + func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) { + + } +} From 53be32e8ad05547448dd2a8717392385b18f8447 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 16:18:19 +0100 Subject: [PATCH 05/10] Release 7.144.0-1 (#3540) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a72f2588fa..21ee89d124 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9185,7 +9185,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9222,7 +9222,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9312,7 +9312,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9339,7 +9339,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9488,7 +9488,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9513,7 +9513,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9582,7 +9582,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9616,7 +9616,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9649,7 +9649,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9679,7 +9679,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9989,7 +9989,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10020,7 +10020,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10048,7 +10048,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10081,7 +10081,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10111,7 +10111,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10144,11 +10144,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10381,7 +10381,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10408,7 +10408,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10440,7 +10440,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10477,7 +10477,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10512,7 +10512,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10547,11 +10547,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10724,11 +10724,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10757,10 +10757,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 9005f0babff02e1051725ddf3683592e347f956f Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 17:21:30 +0100 Subject: [PATCH 06/10] Change save password Never for Site button to Not Now (#3471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1202926619870900/1208592991532541/f **Description**: Change the Never Save button with Not Now on mobile platforms to avoid users accidentally disabling for their top sites on first usage. Update the logic accordingly. Send dismiss pixel on press of the Never Save button. **Steps to test this PR**: 1. Clear your autofill data from the debug menu 2. Go to https://fill.dev/form/login-simple and submit some details. 3. **Make sure there is no "Never Ask for This Site" button and there is a "Not Now" button** 4. Tap the "Not Now" button. 5. Repeat step 2. 6. **Make sure the Save prompt shows again** **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --------- Co-authored-by: amddg44 --- DuckDuckGo/SaveLoginView.swift | 10 ++++++++-- DuckDuckGo/UserText.swift | 1 + DuckDuckGo/bg.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/cs.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/da.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/de.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/el.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/en.lproj/Localizable.strings | 3 +++ DuckDuckGo/es.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/et.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/fi.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/fr.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/hr.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/hu.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/it.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/lt.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/lv.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/nb.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/nl.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/pl.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/pt.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/ro.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/ru.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/sk.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/sl.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/sv.lproj/Localizable.strings | 6 ++++++ DuckDuckGo/tr.lproj/Localizable.strings | 8 +++++++- 27 files changed, 157 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/SaveLoginView.swift b/DuckDuckGo/SaveLoginView.swift index 9d15b185d4..2a738213d0 100644 --- a/DuckDuckGo/SaveLoginView.swift +++ b/DuckDuckGo/SaveLoginView.swift @@ -194,9 +194,15 @@ struct SaveLoginView: View { VStack(spacing: Const.Size.ctaVerticalSpacing) { AutofillViews.PrimaryButton(title: confirmButton, action: viewModel.save) + switch layoutType { + case .newUser: + AutofillViews.TertiaryButton(title: UserText.autofillSaveLoginNoThanksCTA, + action: viewModel.cancelButtonPressed) + default: + AutofillViews.TertiaryButton(title: UserText.autofillSaveLoginNeverPromptCTA, + action: viewModel.neverPrompt) + } - AutofillViews.TertiaryButton(title: UserText.autofillSaveLoginNeverPromptCTA, - action: viewModel.neverPrompt) } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 331dc45c98..15bef3837f 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -419,6 +419,7 @@ public struct UserText { public static let autofillOnboardingKeyFeaturesSecureStorageDescription = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description", value: "Passwords are encrypted, stored on device, and locked with Face ID or passcode.", comment: "Description of autofill onboarding prompt's secure storage feature") public static let autofillOnboardingKeyFeaturesSyncTitle = NSLocalizedString("autofill.onboarding.key-features.sync.title", value: "Sync between devices", comment: "Title of autofill onboarding prompt's sync feature") public static let autofillOnboardingKeyFeaturesSyncDescription = NSLocalizedString("autofill.onboarding.key-features.sync.description", value: "End-to-end encrypted and easy to set up when you’re ready.", comment: "Description of autofill onboarding prompt's sync feature") + public static let autofillSaveLoginNoThanksCTA = NSLocalizedString("autofill.save-login.no-thanks.CTA", value: "No Thanks", comment: "CTA displayed on modal asking if the user wants to dismiss the save login action for now") public static let autofillSavePasswordSaveCTA = NSLocalizedString("autofill.save-password.save.CTA", value: "Save Password", comment: "Confirm CTA displayed on modal asking for the user to save the password") public static let autofillUpdatePasswordSaveCTA = NSLocalizedString("autofill.update-password.save.CTA", value: "Update Password", comment: "Confirm CTA displayed on modal asking for the user to update the password") diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index c83ff1b255..fe5c65f295 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Търсене на пароли"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Паролите са криптирани. Никой освен Вас не може да ги види, дори ние."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Паролите са криптирани. Никой освен Вас не може да ги види, дори ние. [Научете повече](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Запазване на тази парола?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Не, благодаря"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "С DuckDuckGo Passwords & Autofill можете да съхранявате паролата по сигурен начин на устройството."; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index 129c113c73..45df7910f4 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Prohledat hesla"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Hesla jsou šifrovaná. Nikdo kromě tebe je nevidí, dokonce ani my."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Hesla jsou šifrovaná. Nikdo kromě tebe je nevidí, dokonce ani my. [Další informace](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Uložit tohle heslo?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, děkuji"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Bezpečně si ulož heslo do zařízení pomocí funkce pro ukládání a automatické vyplňování hesel DuckDuckGo."; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index 4813d438e4..382ee89485 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Søg adgangskoder"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Adgangskoderne er krypterede. Ingen andre end dig kan se dem, ikke engang os."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Adgangskoderne er krypterede. Ingen andre end dig kan se dem, ikke engang os. [Få flere oplysninger](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Gem denne adgangskode?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nej tak."; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Gem din adgangskode sikkert på enheden med DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 9c0f7ef2f1..5344dede4c 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Passwörter suchen"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Passwörter sind verschlüsselt. Niemand außer dir kann sie sehen, nicht einmal wir."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Passwörter sind verschlüsselt. Niemand außer dir kann sie sehen, nicht einmal wir. [Mehr erfahren](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Dieses Passwort speichern?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nein, danke"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Speichere dein Passwort mit DuckDuckGo Passwörter & Autovervollständigen sicher auf dem Gerät."; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 97567e39db..70c05970bd 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Αναζήτηση κωδικών πρόσβασης"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Οι κωδικοί πρόσβασης είναι κρυπτογραφημένοι. Κανείς άλλος εκτός από εσάς δεν μπορεί να τους βλέπει, ούτε καν εμείς."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Οι κωδικοί πρόσβασης είναι κρυπτογραφημένοι. Κανείς άλλος εκτός από εσάς δεν μπορεί να τους βλέπει, ούτε καν εμείς. [Μάθετε περισσότερα](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Αποθήκευση αυτού του κωδικού πρόσβασης;"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Όχι, ευχαριστώ"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Αποθηκεύστε με ασφάλεια τον κωδικό πρόσβασής σας στη συσκευή με τη λειτουργία DuckDuckGo κωδικοί πρόσβασης και αυτόματη συμπλήρωση."; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index b4b69e4eb4..2ff7e7ca21 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -593,6 +593,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Save this password?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "No Thanks"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Securely store your password on device with DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 85f5cff2a9..0b7bd2e9e0 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Buscar contraseñas"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Las contraseñas están cifradas. Nadie más que tú puede verlas, ni siquiera nosotros."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Las contraseñas están cifradas. Nadie más que tú puede verlas, ni siquiera nosotros. [Más información](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "¿Guardar esta contraseña?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "No, gracias"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Almacena de forma segura tu contraseña en el dispositivo con DuckDuckGo Contraseñas y Autocompletar."; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index f161bdab3f..38a5acaea0 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Otsi paroole"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Paroolid on krüpteeritud. Keegi peale sinu ei näe neid, isegi mitte meie."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Paroolid on krüpteeritud. Keegi peale sinu ei näe neid, isegi mitte meie. [Lisateave](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Kas salvestada see parool?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ei aitäh"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Salvesta oma parool turvaliselt seadmesse DuckDuckGo paroolide ja automaatse täitmisega."; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 9667b6d878..d0c31a4164 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Etsi salasanoja"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Salasanat salataan. Kukaan muu kuin sinä ei näe niitä, emme edes me."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Salasanat salataan. Kukaan muu kuin sinä ei näe niitä, emme edes me. [Lisätietoja](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Tallennetaanko tämä salasana?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ei kiitos"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Tallenna salasanasi turvallisesti laitteeseen DuckDuckGon salasanojen ja automaattisen täytön avulla."; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index a4797bc797..f900fa4815 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Rechercher un mot de passe"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Les mots de passe sont cryptés. Personne d'autre que vous ne peut les voir, pas même nous."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Les mots de passe sont cryptés. Personne d'autre que vous ne peut les voir, pas même nous. [En savoir plus](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Enregistrer ce mot de passe ?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Non merci"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Stockez votre mot de passe en toute sécurité sur votre appareil avec DuckDuckGo, Mots de passe et saisie automatique."; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index bbbc5ada7a..269e54b844 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Pretraživanje lozinki"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Lozinke su šifrirane. Nitko osim tebe ne može ih vidjeti, čak ni mi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Lozinke su šifrirane. Nitko osim tebe ne može ih vidjeti, čak ni mi. [Saznaj više](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Želiš li spremiti ovu lozinku?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, hvala"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Sigurno pohrani svoju lozinku na uređaj pomoću usluge automatskog popunjavanja DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 541241f5d6..52618f6a75 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Jelszavak keresése"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "A jelszavak titkosítva vannak. Rajtad kívül senki sem láthatja őket, még mi sem."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "A jelszavak titkosítva vannak. Rajtad kívül senki sem láthatja őket, még mi sem. [További információk](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Mented a jelszót?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nem, köszönöm"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Tárold biztonságosan a jelszavaidat az eszközödön a DuckDuckGo Jelszavak és automatikus kitöltés funkciójával."; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index 4cffed59af..e01a9d279f 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Cerca password"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Le password sono crittografate. Nessuno tranne te può vederle, nemmeno noi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Le password sono crittografate. Nessuno tranne te può vederle, nemmeno noi. [Scopri di più](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Salvare questa password?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "No, grazie"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Memorizza in modo sicuro la tua password sul dispositivo con DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index bad7486d4d..c116362c61 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Ieškoti slaptažodžių"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Slaptažodžiai yra užšifruoti. Niekas, išskyrus jus, negali jų matyti – net mes."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Slaptažodžiai yra užšifruoti. Niekas, išskyrus jus, negali jų matyti – net mes. [Sužinoti daugiau](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Išsaugoti šį slaptažodį?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, dėkoju"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Saugiai išsaugokite slaptažodį įrenginyje naudodami „DuckDuckGo“ slaptažodžių ir automatinio pildymo parinktį."; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 397c81bfcb..8f9c980cc1 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Meklēt paroles"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Paroles ir šifrētas. Neviens, izņemot tevi, tās nevar redzēt – pat mēs ne."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Paroles ir šifrētas. Neviens, izņemot tevi, tās nevar redzēt – pat mēs ne. [Uzzini vairāk](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Vai saglabāt šo paroli?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nē, paldies"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Droši saglabā paroli ierīcē, izmantojot DuckDuckGo paroles un automātisko aizpildīšanu."; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index 985493c5f8..cd0ded480c 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Søk i passord"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Passord krypteres. Ingen andre enn du kan se dem, ikke engang vi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Passord krypteres. Ingen andre enn du kan se dem, ikke engang vi. [Finn ut mer](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Vil du lagre dette passordet?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nei takk"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Lagre passordet ditt trygt på enheten med DuckDuckGos passord og autofyll."; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index a710d2848d..0ae46ad07e 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Wachtwoorden zoeken"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Wachtwoorden worden versleuteld. Niemand anders kan ze zien, zelfs wij niet."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Wachtwoorden worden versleuteld. Niemand anders dan jij kunt ze zien, zelfs wij niet. [Meer informatie](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Dit wachtwoord opslaan?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nee, bedankt"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Sla je wachtwoord veilig op je apparaat op met DuckDuckGo wachtwoorden en automatisch invullen."; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 183724755d..9888d7bfe0 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Wyszukaj hasła"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Hasła są szyfrowane. Nikt poza Tobą ich nie widzi, nawet my."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Hasła są szyfrowane. Nikt poza Tobą ich nie widzi, nawet my. [Więcej informacji] (DDGQuickLink: //duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Zapisać to hasło?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nie, dziękuję"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Bezpiecznie przechowuj swoje hasło na urządzeniu dzięki funkcji Hasła i autouzupełnianie DuckDuckGo."; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index d70ed96b69..7fd8ef149b 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Pesquisar palavras-passe"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "As palavras-passe estão encriptadas. Ninguém além de ti pode vê-las, nem mesmo nós."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "As palavras-passe estão encriptadas. Ninguém além de ti pode vê-las, nem mesmo nós. [Sabe mais](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Guardar esta palavra-passe?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Não, obrigado"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Guarda a tua palavra-passe com segurança no dispositivo com a funcionalidade Palavras-passe e preenchimento automático do DuckDuckGo."; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index a86f184e7b..ee562a26d3 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Caută parole"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Parolele sunt criptate. Nimeni în afară de tine nu le poate vedea, nici măcar noi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Parolele sunt criptate. Nimeni în afară de tine nu le poate vedea, nici măcar noi. [Află mai multe](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Dorești să salvezi această parolă?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nu, mulțumesc"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Stochează în siguranță parola pe dispozitiv, cu Parole și completare automată DuckDuckGo."; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 065fa8a045..5d041fb58f 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Найти пароль"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Пароли подвергаются шифрованию. Никто, кроме вас, их не увидит. Даже мы."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Пароли подвергаются шифрованию. Никто, кроме вас, их не увидит. Даже мы. [Подробнее...](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Сохранить пароль?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Нет, спасибо"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Вы можете сохранить этот пароль на устройстве под надежной защитой функции «Пароли и автозаполнение» от DuckDuckGo."; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 2f75da82d3..43c3005757 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Vyhľadávanie hesiel"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Heslá sú zašifrované. Nikto okrem vás ich nemôže vidieť, dokonca ani my."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Heslá sú zašifrované. Nikto okrem vás ich nemôže vidieť, dokonca ani my. [Viac informácií](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Uložiť toto heslo?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nie, ďakujem"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Heslo bezpečne uložte do zariadenia pomocou aplikácie DuckDuckGo Passwords & Autofill."; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 1d67ca1159..088ce79181 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Iskanje gesel"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Gesla so šifrirana. Nihče razen vas jih ne more videti, niti mi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Gesla so šifrirana. Nihče razen vas jih ne more videti, niti mi. [Več o tem](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Želite shraniti to geslo?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Ne, hvala"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "S funkcijo DuckDuckGo Passwords & Autofill varno shranite geslo v napravo."; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index c847aaf2d1..de228cd093 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Sök lösenord"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Lösenorden är krypterade. Ingen annan än du kan se dem, inte ens vi."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Lösenorden är krypterade. Ingen annan än du kan se dem, inte ens vi. [Läs mer](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Spara det här lösenordet?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Nej tack"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "Förvara ditt lösenord säkert på enheten med DuckDuckGo Lösenord och autofyll."; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index de8681a50b..5b967dc65a 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -428,7 +428,7 @@ "autofill.logins.empty-view.button.title" = "Şifreleri İçe Aktar"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle.first.paragraph" = "Kayıtlı parolaları başka bir tarayıcıdan DuckDuckGo'ya aktarabilirsiniz."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Kayıtlı parolaları başka bir tarayıcıdan DuckDuckGo'ya aktarabilirsiniz."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Henüz şifre kaydedilmedi"; @@ -472,6 +472,9 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Şifreleri ara"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Parolalar şifrelenir. Onları sizden başka kimse göremez. Biz bile."; + /* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ "autofill.logins.list.settings.footer.markdown" = "Parolalar şifrelenir. Onları sizden başka kimse göremez. Biz bile. [Daha Fazla Bilgi](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; @@ -596,6 +599,9 @@ /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Bu parola kaydedilsin mi?"; +/* CTA displayed on modal asking if the user wants to dismiss the save login action for now */ +"autofill.save-login.no-thanks.CTA" = "Hayır Teşekkürler"; + /* Message displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.security.message" = "DuckDuckGo Parolalar ve Otomatik Doldurma ile parolanızı cihazınızda güvenle saklayın."; From eb72d5429bdf2aca1ac737770527c8c203d10007 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 5 Nov 2024 17:39:19 +0100 Subject: [PATCH 07/10] Update C-S-S to 6.29.0 (#3541) Task/Issue URL: https://app.asana.com/0/1201048563534612/1208699974934565/f Description: This C-S-S release adds code for macOS. There are no changes to iOS. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bed6f3b36b..31a0150a02 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10982,7 +10982,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 203.1.0; + version = 203.3.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0be6bd842d..b8f119becf 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "19f1e5c945aa92562ad2d087e8d6c99801edf656", - "version" : "203.1.0" + "revision" : "64a5d8d1e19951fe397305a14e521713fb0eaa49", + "version" : "203.3.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "48fee2508995d4ac02d18b3d55424adedcb4ce4f", - "version" : "6.28.0" + "revision" : "6cab7bdb584653a5dc007cc1ae827ec41c5a91bc", + "version" : "6.29.0" } }, { From b50b7fa93417d6296a8788d9222a0db1e3c7a31b Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 5 Nov 2024 17:51:26 +0100 Subject: [PATCH 08/10] Onboarding Add to Dock Refactor for Intro scenario (#3538) Task/Issue URL: https://app.asana.com/0/72649045549333/1208648960421864/f **Description**: Refactor the logic to show Add to Dock from the Onboarding Intro or the end of the contextual flow. --- Core/UserDefaultsPropertyWrapper.swift | 1 - DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/AppSettings.swift | 2 +- DuckDuckGo/AppUserDefaults.swift | 12 +- DuckDuckGo/DaxDialogs.swift | 13 ++- DuckDuckGo/MainViewController.swift | 2 +- DuckDuckGo/NewTabPageViewController.swift | 5 +- DuckDuckGo/OnboardingDebugView.swift | 52 ++++++++- .../AddToDock/AddToDockTutorialView.swift | 6 +- .../OnboardingView+AddToDockContent.swift | 96 ++++++++++++++++ .../ContextualOnboardingDialogs.swift | 17 ++- .../NewTabDaxDialogFactory.swift | 13 ++- .../ContextualDaxDialogsFactory.swift | 13 ++- .../Manager/OnboardingManager.swift | 35 ++++-- .../OnboardingIntroViewModel.swift | 20 +++- .../OnboardingIntro/OnboardingView.swift | 13 +++ DuckDuckGo/UserText.swift | 7 ++ DuckDuckGo/en.lproj/Localizable.strings | 12 ++ DuckDuckGoTests/AppSettingsMock.swift | 4 +- .../ContextualDaxDialogsFactoryTests.swift | 36 +++++- ...alOnboardingNewTabDialogFactoryTests.swift | 40 ++++++- DuckDuckGoTests/DaxDialogTests.swift | 50 ++++++++- .../OnboardingIntroViewModelTests.swift | 30 +++++ DuckDuckGoTests/OnboardingManagerMock.swift | 3 +- DuckDuckGoTests/OnboardingManagerTests.swift | 104 +++++++++++++----- .../TabViewControllerDaxDialogTests.swift | 1 + 26 files changed, 516 insertions(+), 75 deletions(-) create mode 100644 DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 0f625c97e3..1231840c9b 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -171,7 +171,6 @@ public struct UserDefaultsWrapper { // Debug keys case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled" case debugOnboardingHighlightsEnabledKey = "com.duckduckgo.ios.debug.onboardingHighlightsEnabled" - case debugOnboardingAddToDockEnabledKey = "com.duckduckgo.ios.debug.onboardingAddToDockEnabled" // Duck Player Pixel Experiment case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed.v2" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 31a0150a02..34ac5cf757 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -713,6 +713,7 @@ 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; }; + 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */; }; 9F4CC5152C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */; }; 9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */; }; 9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */; }; @@ -2514,6 +2515,7 @@ 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = ""; }; + 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddToDockContent.swift"; sourceTree = ""; }; 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterMock.swift; sourceTree = ""; }; 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerDaxDialogTests.swift; sourceTree = ""; }; 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabDelegate.swift; sourceTree = ""; }; @@ -4810,6 +4812,7 @@ 9F8E0F302CCA6390001EA7C5 /* AddToDockTutorialView.swift */, 9F8E0F372CCFAA8A001EA7C5 /* AddToDockPromoView.swift */, 9F8E0F3C2CCFD071001EA7C5 /* AddToDockPromoViewModel.swift */, + 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */, ); path = AddToDock; sourceTree = ""; @@ -7408,6 +7411,7 @@ F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, + 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index 2e212f2e59..af3f91b5a4 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -87,5 +87,5 @@ protocol AppSettings: AnyObject, AppDebugSettings { protocol AppDebugSettings { var onboardingHighlightsEnabled: Bool { get set } - var onboardingAddToDockEnabled: Bool { get set } + var onboardingAddToDockState: OnboardingAddToDockState { get set } } diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 9d1ca894a3..4cd79c3e0b 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -84,6 +84,7 @@ public class AppUserDefaults: AppSettings { private struct DebugKeys { static let inspectableWebViewsEnabledKey = "com.duckduckgo.ios.debug.inspectableWebViewsEnabled" static let autofillDebugScriptEnabledKey = "com.duckduckgo.ios.debug.autofillDebugScriptEnabled" + static let onboardingAddToDockStateKey = "com.duckduckgo.ios.debug.onboardingAddToDockState" } private var userDefaults: UserDefaults? { @@ -422,8 +423,15 @@ public class AppUserDefaults: AppSettings { @UserDefaultsWrapper(key: .debugOnboardingHighlightsEnabledKey, defaultValue: false) var onboardingHighlightsEnabled: Bool - @UserDefaultsWrapper(key: .debugOnboardingAddToDockEnabledKey, defaultValue: false) - var onboardingAddToDockEnabled: Bool + var onboardingAddToDockState: OnboardingAddToDockState { + get { + guard let rawValue = userDefaults?.string(forKey: DebugKeys.onboardingAddToDockStateKey) else { return .disabled } + return OnboardingAddToDockState(rawValue: rawValue) ?? .disabled + } + set { + userDefaults?.set(newValue.rawValue, forKey: DebugKeys.onboardingAddToDockStateKey) + } + } } extension AppUserDefaults: AppConfigurationFetchStatistics { diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index d1793efb6b..d1d30b9e3a 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -41,6 +41,7 @@ protocol ContextualOnboardingLogic { var shouldShowPrivacyButtonPulse: Bool { get } var isShowingSearchSuggestions: Bool { get } var isShowingSitesSuggestions: Bool { get } + var isShowingAddToDockDialog: Bool { get } func setSearchMessageSeen() func setFireEducationMessageSeen() @@ -211,6 +212,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { private var settings: DaxDialogsSettings private var entityProviding: EntityProviding private let variantManager: VariantManager + private let addToDockManager: OnboardingAddToDockManaging private var nextHomeScreenMessageOverride: HomeScreenSpec? @@ -222,10 +224,13 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { /// Use singleton accessor, this is only accessible for tests init(settings: DaxDialogsSettings = DefaultDaxDialogsSettings(), entityProviding: EntityProviding, - variantManager: VariantManager = DefaultVariantManager()) { + variantManager: VariantManager = DefaultVariantManager(), + onboardingManager: OnboardingAddToDockManaging = OnboardingManager() + ) { self.settings = settings self.entityProviding = entityProviding self.variantManager = variantManager + self.addToDockManager = onboardingManager } private var isNewOnboarding: Bool { @@ -276,6 +281,11 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return lastShownDaxDialogType.flatMap(BrowsingSpec.SpecType.init(rawValue:)) == .visitWebsite || currentHomeSpec == .subsequent } + var isShowingAddToDockDialog: Bool { + guard isNewOnboarding else { return false } + return currentHomeSpec == .final && addToDockManager.addToDockEnabledState == .contextual + } + var isEnabled: Bool { // skip dax dialogs in integration tests guard ProcessInfo.processInfo.environment["DAXDIALOGS"] != "false" else { return false } @@ -733,6 +743,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { private func clearOnboardingBrowsingData() { removeLastShownDaxDialog() removeLastVisitedOnboardingWebsite() + currentHomeSpec = nil } } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index d1a2fd2e36..7800b7cc2a 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -2672,7 +2672,7 @@ extension MainViewController: AutoClearWorker { // Ideally this should happen once data clearing has finished AND the animation is finished if showNextDaxDialog { self.newTabPageViewController?.showNextDaxDialog() - } else if KeyboardSettings().onNewTab { + } else if KeyboardSettings().onNewTab && !self.contextualOnboardingLogic.isShowingAddToDockDialog { // If we're showing the Add to Dock dialog prevent address bar to become first responder. We want to make sure the user focues on the Add to Dock instructions. let showKeyboardAfterFireButton = DispatchWorkItem { self.enterSearch() } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index 0f24599de6..8c02560816 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -310,9 +310,12 @@ extension NewTabPageViewController { guard let spec = dialogProvider.nextHomeScreenMessageNew() else { return } - let onDismiss = { + let onDismiss = { [weak self] in + guard let self else { return } dialogProvider.dismiss() self.dismissHostingController(didFinishNTPOnboarding: true) + // Make the address bar first responder after closing the new tab page final dialog. + self.launchNewSearch() } let daxDialogView = AnyView(factory.createDaxDialog(for: spec, onDismiss: onDismiss)) let hostingController = UIHostingController(rootView: daxDialogView) diff --git a/DuckDuckGo/OnboardingDebugView.swift b/DuckDuckGo/OnboardingDebugView.swift index 88976baa81..f6528aea25 100644 --- a/DuckDuckGo/OnboardingDebugView.swift +++ b/DuckDuckGo/OnboardingDebugView.swift @@ -22,6 +22,7 @@ import SwiftUI struct OnboardingDebugView: View { @StateObject private var viewModel = OnboardingDebugViewModel() + @State private var isShowingResetDaxDialogsAlert = false private let newOnboardingIntroStartAction: () -> Void @@ -45,8 +46,13 @@ struct OnboardingDebugView: View { } Section { - Toggle( - isOn: $viewModel.isOnboardingAddToDockLocalFlagEnabled, + Picker( + selection: $viewModel.onboardingAddToDockLocalFlagState, + content: { + ForEach(OnboardingAddToDockState.allCases) { state in + Text(verbatim: state.description).tag(state) + } + }, label: { Text(verbatim: "Onboarding Add to Dock local setting enabled") } @@ -57,6 +63,18 @@ struct OnboardingDebugView: View { Text(verbatim: "Requires internal user flag set to have an effect.") } + Section { + Button(action: { + viewModel.resetDaxDialogs() + isShowingResetDaxDialogsAlert = true + }, label: { + Text(verbatim: "Reset Dax Dialogs State") + }) + .alert(isPresented: $isShowingResetDaxDialogsAlert, content: { + Alert(title: Text(verbatim: "Dax Dialogs reset"), dismissButton: .cancel()) + }) + } + Section { Button(action: newOnboardingIntroStartAction, label: { let onboardingType = viewModel.isOnboardingHighlightsLocalFlagEnabled ? "Highlights" : "" @@ -74,22 +92,44 @@ final class OnboardingDebugViewModel: ObservableObject { } } - @Published var isOnboardingAddToDockLocalFlagEnabled: Bool { + @Published var onboardingAddToDockLocalFlagState: OnboardingAddToDockState { didSet { - manager.isAddToDockLocalFlagEnabled = isOnboardingAddToDockLocalFlagEnabled + manager.addToDockLocalFlagState = onboardingAddToDockLocalFlagState } } private let manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging + private var settings: DaxDialogsSettings - init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager()) { + init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager(), settings: DaxDialogsSettings = DefaultDaxDialogsSettings()) { self.manager = manager + self.settings = settings isOnboardingHighlightsLocalFlagEnabled = manager.isOnboardingHighlightsLocalFlagEnabled - isOnboardingAddToDockLocalFlagEnabled = manager.isAddToDockLocalFlagEnabled + onboardingAddToDockLocalFlagState = manager.addToDockLocalFlagState } + func resetDaxDialogs() { + settings.isDismissed = false + settings.homeScreenMessagesSeen = 0 + settings.browsingAfterSearchShown = false + settings.browsingWithTrackersShown = false + settings.browsingWithoutTrackersShown = false + settings.browsingMajorTrackingSiteShown = false + settings.fireMessageExperimentShown = false + settings.fireButtonPulseDateShown = nil + settings.privacyButtonPulseShown = false + settings.browsingFinalDialogShown = false + settings.lastVisitedOnboardingWebsiteURLPath = nil + settings.lastShownContextualOnboardingDialogType = nil + } } #Preview { OnboardingDebugView(onNewOnboardingIntroStartAction: {}) } + +extension OnboardingAddToDockState: Identifiable { + var id: OnboardingAddToDockState { + self + } +} diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift index e13ed55c9f..8b0a2a07bf 100644 --- a/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift @@ -32,6 +32,7 @@ struct AddToDockTutorialView: View { private let title: String private let message: String + private let cta: String private let action: () -> Void @State private var animateTitle = true @@ -44,10 +45,12 @@ struct AddToDockTutorialView: View { init( title: String, message: String, + cta: String, action: @escaping () -> Void ) { self.title = title self.message = message + self.cta = cta self.action = action } @@ -81,7 +84,7 @@ struct AddToDockTutorialView: View { } Button(action: action) { - Text(UserText.AddToDockOnboarding.Buttons.dismiss) + Text(cta) } .buttonStyle(PrimaryButtonStyle()) .visibility(showContent ? .visible : .invisible) @@ -110,6 +113,7 @@ struct AddToDockTutorial_Previews: PreviewProvider { AddToDockTutorialView( title: UserText.AddToDockOnboarding.Tutorial.title, message: UserText.AddToDockOnboarding.Tutorial.message, + cta: UserText.AddToDockOnboarding.Buttons.dismiss, action: {} ) .padding() diff --git a/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift new file mode 100644 index 0000000000..8baf1f18c1 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AddToDock/OnboardingView+AddToDockContent.swift @@ -0,0 +1,96 @@ +// +// OnboardingView+AddToDockContent.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Onboarding + +extension OnboardingView { + + struct AddToDockPromoContentState { + var animateTitle = true + var animateMessage = false + var showContent = false + } + + struct AddToDockPromoContent: View { + + @State private var showAddToDockTutorial = false + + private var animateTitle: Binding + private var animateMessage: Binding + private var showContent: Binding + private let dismissAction: (_ fromAddToDock: Bool) -> Void + + init( + animateTitle: Binding = .constant(true), + animateMessage: Binding = .constant(true), + showContent: Binding = .constant(false), + dismissAction: @escaping (_ fromAddToDock: Bool) -> Void + ) { + self.animateTitle = animateTitle + self.animateMessage = animateMessage + self.showContent = showContent + self.dismissAction = dismissAction + } + + var body: some View { + if showAddToDockTutorial { + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Intro.tutorialDismissCTA) { + dismissAction(true) + } + } else { + ContextualDaxDialogContent( + title: UserText.AddToDockOnboarding.Intro.title, + titleFont: Font(UIFont.daxTitle3()), + message: NSAttributedString(string: UserText.AddToDockOnboarding.Intro.message), + messageFont: Font.system(size: 16), + customView: AnyView(addToDockPromoView), + customActionView: AnyView(customActionView) + ) + } + } + + private var addToDockPromoView: some View { + AddToDockPromoView() + .aspectRatio(contentMode: .fit) + .padding(.vertical) + } + + private var customActionView: some View { + VStack { + OnboardingCTAButton( + title: UserText.AddToDockOnboarding.Buttons.addToDockTutorial, + action: { + showAddToDockTutorial = true + } + ) + + OnboardingCTAButton( + title: UserText.AddToDockOnboarding.Intro.skipCTA, + buttonStyle: .ghost, + action: { + dismissAction(false) + } + ) + } + } + + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index f5bb5d47ec..eb3aee205b 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -185,10 +185,10 @@ struct OnboardingTrackersDoneDialog: View { struct OnboardingFinalDialog: View { let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenTitle - let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton let logoPosition: DaxDialogLogoPosition let message: String + let cta: String let canShowAddToDockTutorial: Bool let dismissAction: (_ fromAddToDock: Bool) -> Void @@ -198,7 +198,7 @@ struct OnboardingFinalDialog: View { ScrollView(.vertical, showsIndicators: false) { DaxDialogView(logoPosition: logoPosition) { if showAddToDockTutorial { - OnboardingAddToDockTutorialContent { + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Buttons.dismiss) { dismissAction(true) } } else { @@ -206,6 +206,7 @@ struct OnboardingFinalDialog: View { title: title, titleFont: Font(UIFont.daxTitle3()), message: NSAttributedString(string: message), + messageFont: Font.system(size: 16), customView: AnyView(customView), customActionView: AnyView(customActionView) ) @@ -277,15 +278,17 @@ struct OnboardingCTAButton: View { struct OnboardingAddToDockTutorialContent: View { let title = UserText.AddToDockOnboarding.Tutorial.title let message = UserText.AddToDockOnboarding.Tutorial.message - let cta = UserText.AddToDockOnboarding.Buttons.dismiss + let cta: String let dismissAction: () -> Void var body: some View { AddToDockTutorialView( title: title, message: message, - action: dismissAction) + cta: cta, + action: dismissAction + ) } } @@ -322,6 +325,7 @@ struct OnboardingAddToDockTutorialContent: View { OnboardingFinalDialog( logoPosition: .top, message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, + cta: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton, canShowAddToDockTutorial: false, dismissAction: { _ in } ) @@ -332,6 +336,7 @@ struct OnboardingAddToDockTutorialContent: View { OnboardingFinalDialog( logoPosition: .left, message: UserText.AddToDockOnboarding.EndOfJourney.message, + cta: UserText.AddToDockOnboarding.Buttons.dismiss, canShowAddToDockTutorial: true, dismissAction: { _ in } ) @@ -353,11 +358,11 @@ struct OnboardingAddToDockTutorialContent: View { } #Preview("Add To Dock Tutorial - Light") { - OnboardingAddToDockTutorialContent(dismissAction: {}) + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Buttons.dismiss, dismissAction: {}) .preferredColorScheme(.light) } #Preview("Add To Dock Tutorial - Dark") { - OnboardingAddToDockTutorialContent(dismissAction: {}) + OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Buttons.dismiss, dismissAction: {}) .preferredColorScheme(.dark) } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index c4f74f8217..4d1a59eb66 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -99,14 +99,19 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { } private func createFinalDialog(onDismiss: @escaping () -> Void) -> some View { - let message = if onboardingManager.isAddToDockEnabled { - UserText.AddToDockOnboarding.EndOfJourney.message + let shouldShowAddToDock = onboardingManager.addToDockEnabledState == .contextual + + let (message, cta) = if shouldShowAddToDock { + (UserText.AddToDockOnboarding.EndOfJourney.message, UserText.AddToDockOnboarding.Buttons.dismiss) } else { - onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + ( + onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, + UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton + ) } return FadeInView { - OnboardingFinalDialog(logoPosition: .top, message: message, canShowAddToDockTutorial: onboardingManager.isAddToDockEnabled) { [weak self] isDismissedFromAddToDock in + OnboardingFinalDialog(logoPosition: .top, message: message, cta: cta, canShowAddToDockTutorial: shouldShowAddToDock) { [weak self] isDismissedFromAddToDock in if isDismissedFromAddToDock { Logger.onboarding.debug("Dismissed from add to dock") } else { diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index b6b9f08289..5f0b58538c 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -182,13 +182,18 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } private func endOfJourneyDialog(delegate: ContextualOnboardingDelegate, pixelName: Pixel.Event) -> some View { - let message = if onboardingManager.isAddToDockEnabled { - UserText.AddToDockOnboarding.EndOfJourney.message + let shouldShowAddToDock = onboardingManager.addToDockEnabledState == .contextual + + let (message, cta) = if shouldShowAddToDock { + (UserText.AddToDockOnboarding.EndOfJourney.message, UserText.AddToDockOnboarding.Buttons.dismiss) } else { - onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + ( + onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, + UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton + ) } - return OnboardingFinalDialog(logoPosition: .left, message: message, canShowAddToDockTutorial: onboardingManager.isAddToDockEnabled, dismissAction: { [weak delegate, weak self] isDismissedFromAddToDock in + return OnboardingFinalDialog(logoPosition: .left, message: message, cta: cta, canShowAddToDockTutorial: shouldShowAddToDock, dismissAction: { [weak delegate, weak self] isDismissedFromAddToDock in delegate?.didTapDismissContextualOnboardingAction() if isDismissedFromAddToDock { Logger.onboarding.debug("Dismissed from add to dock") diff --git a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift index f3ab2408a4..b2f212410f 100644 --- a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift +++ b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift @@ -20,6 +20,23 @@ import BrowserServicesKit import Core +enum OnboardingAddToDockState: String, Equatable, CaseIterable, CustomStringConvertible { + case disabled + case intro + case contextual + + var description: String { + switch self { + case .disabled: + "Disabled" + case .intro: + "Onboarding Intro" + case .contextual: + "Dax Dialogs" + } + } +} + final class OnboardingManager { private var appDefaults: AppDebugSettings private let featureFlagger: FeatureFlagger @@ -75,27 +92,29 @@ extension OnboardingManager: OnboardingHighlightsManaging, OnboardingHighlightsD // MARK: - Add to Dock protocol OnboardingAddToDockManaging: AnyObject { - var isAddToDockEnabled: Bool { get } + var addToDockEnabledState: OnboardingAddToDockState { get } } protocol OnboardingAddToDockDebugging: AnyObject { - var isAddToDockLocalFlagEnabled: Bool { get set } + var addToDockLocalFlagState: OnboardingAddToDockState { get set } var isAddToDockFeatureFlagEnabled: Bool { get } } extension OnboardingManager: OnboardingAddToDockManaging, OnboardingAddToDockDebugging { - var isAddToDockEnabled: Bool { - // TODO: Add variant condition once the experiment is setup - isIphone && isAddToDockLocalFlagEnabled && isAddToDockFeatureFlagEnabled + var addToDockEnabledState: OnboardingAddToDockState { + // TODO: Add variant condition OR local conditions + guard isAddToDockFeatureFlagEnabled && isIphone else { return .disabled } + + return addToDockLocalFlagState } - var isAddToDockLocalFlagEnabled: Bool { + var addToDockLocalFlagState: OnboardingAddToDockState { get { - appDefaults.onboardingAddToDockEnabled + appDefaults.onboardingAddToDockState } set { - appDefaults.onboardingAddToDockEnabled = newValue + appDefaults.onboardingAddToDockState = newValue } } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index af1622a3d0..349a6152ee 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -31,7 +31,7 @@ final class OnboardingIntroViewModel: ObservableObject { private var introSteps: [OnboardingIntroStep] private let pixelReporter: OnboardingIntroPixelReporting - private let onboardingManager: OnboardingHighlightsManaging + private let onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging private let isIpad: Bool private let urlOpener: URLOpener private let appIconProvider: () -> AppIcon @@ -39,7 +39,7 @@ final class OnboardingIntroViewModel: ObservableObject { init( pixelReporter: OnboardingIntroPixelReporting, - onboardingManager: OnboardingHighlightsManaging = OnboardingManager(), + onboardingManager: OnboardingHighlightsManaging & OnboardingAddToDockManaging = OnboardingManager(), isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, urlOpener: URLOpener = UIApplication.shared, appIconProvider: @escaping () -> AppIcon = { AppIconManager.shared.appIcon }, @@ -52,7 +52,9 @@ final class OnboardingIntroViewModel: ObservableObject { self.appIconProvider = appIconProvider self.addressBarPositionProvider = addressBarPositionProvider - introSteps = if onboardingManager.isOnboardingHighlightsEnabled { + introSteps = if onboardingManager.isOnboardingHighlightsEnabled && onboardingManager.addToDockEnabledState == .intro { + isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsAddToDockIphoneFlow + } else if onboardingManager.isOnboardingHighlightsEnabled { isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsIPhoneFlow } else { OnboardingIntroStep.defaultFlow @@ -85,6 +87,10 @@ final class OnboardingIntroViewModel: ObservableObject { handleSetDefaultBrowserAction() } + func addToDockContinueAction() { + state = makeViewState(for: .appIconSelection) + } + func appIconPickerContinueAction() { if appIconProvider() != .defaultAppIcon { pixelReporter.trackChooseCustomAppIconColor() @@ -130,6 +136,8 @@ private extension OnboardingIntroViewModel { OnboardingView.ViewState.onboarding(.init(type: .startOnboardingDialog, step: .hidden)) case .browserComparison: OnboardingView.ViewState.onboarding(.init(type: .browsersComparisonDialog, step: stepInfo())) + case .addToDockPromo: + OnboardingView.ViewState.onboarding(.init(type: .addToDockPromoDialog, step: stepInfo())) case .appIconSelection: OnboardingView.ViewState.onboarding(.init(type: .chooseAppIconDialog, step: stepInfo())) case .addressBarPositionSelection: @@ -140,7 +148,9 @@ private extension OnboardingIntroViewModel { } func handleSetDefaultBrowserAction() { - if onboardingManager.isOnboardingHighlightsEnabled { + if onboardingManager.addToDockEnabledState == .intro && onboardingManager.isOnboardingHighlightsEnabled { + state = makeViewState(for: .addToDockPromo) + } else if onboardingManager.isOnboardingHighlightsEnabled { state = makeViewState(for: .appIconSelection) pixelReporter.trackChooseAppIconImpression() } else { @@ -157,8 +167,10 @@ private enum OnboardingIntroStep { case browserComparison case appIconSelection case addressBarPositionSelection + case addToDockPromo static let defaultFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison] static let highlightsIPhoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] static let highlightsIPadFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection] + static let highlightsAddToDockIphoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .addToDockPromo, .appIconSelection, .addressBarPositionSelection] } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index cd834e55b2..6599129896 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -40,6 +40,7 @@ struct OnboardingView: View { @State private var appIconPickerContentState = AppIconPickerContentState() @State private var addressBarPositionContentState = AddressBarPositionContentState() + @State private var addToDockPromoContentState = AddToDockPromoContentState() init(model: OnboardingIntroViewModel) { self.model = model @@ -75,6 +76,10 @@ struct OnboardingView: View { case .browsersComparisonDialog: showComparisonButton = true animateComparisonText = false + case .addToDockPromoDialog: + addToDockPromoContentState.animateTitle = false + addToDockPromoContentState.animateMessage = false + addToDockPromoContentState.showContent = true case .chooseAppIconDialog: appIconPickerContentState.animateTitle = false appIconPickerContentState.animateMessage = false @@ -90,6 +95,8 @@ struct OnboardingView: View { introView case .browsersComparisonDialog: browsersComparisonView + case .addToDockPromoDialog: + addToDockPromoView case .chooseAppIconDialog: appIconPickerView case .chooseAddressBarPositionDialog: @@ -151,6 +158,11 @@ struct OnboardingView: View { .onboardingDaxDialogStyle() } + private var addToDockPromoView: some View { + AddToDockPromoContent(dismissAction: { _ in model.addToDockContinueAction() + }) + } + private var appIconPickerView: some View { AppIconPickerContent( animateTitle: $appIconPickerContentState.animateTitle, @@ -231,6 +243,7 @@ extension OnboardingView.ViewState.Intro { enum IntroType: Equatable { case startOnboardingDialog case browsersComparisonDialog + case addToDockPromoDialog case chooseAppIconDialog case chooseAddressBarPositionDialog } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 15bef3837f..f742067903 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1428,5 +1428,12 @@ But if you *do* want a peek under the hood, you can find more information about static let title = NSLocalizedString("contextual.onboarding.addToDock.tutorial.title", value: "Adding me to your Dock is easy.", comment: "The title of the onboarding dialog popup that explains how to add the DDG browser icon to the dock.") static let message = NSLocalizedString("contextual.onboarding.addToDock.tutorial.message", value: "Find or search for the DuckDuckGo icon on your home screen. Then press and drag into place. That’s it!", comment: "The message of the onboarding dialog popup that explains how to add the DDG browser icon to the dock.") } + + public enum Intro { + static let title = NSLocalizedString("onboarding.addToDock.title", value: "Want to add me to your Dock?", comment: "The title of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock.") + static let message = NSLocalizedString("onboarding.addToDock.message", value: "I can paddle into the Dock and perch there until you need me.", comment: "The message of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock.") + static let skipCTA = NSLocalizedString("onboarding.addToDock.cta", value: "Skip", comment: "The title of the dialog button CTA to skip adding the DDB browser icon to the dock.") + static let tutorialDismissCTA = NSLocalizedString("onboarding.addToDock.tutorial.cta", value: "Got It", comment: "Button on the Add to Dock tutorial screen of the onboarding, it will dismiss the screen and proceed to the next step.") + } } } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 2ff7e7ca21..f84a7a780f 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1859,6 +1859,18 @@ https://duckduckgo.com/mac"; /* Text displayed on notification appearing in the address bar when the browser hides a cookie popup */ "omnibar.notification.popup-hidden" = "Pop-up Hidden"; +/* The title of the dialog button CTA to skip adding the DDB browser icon to the dock. */ +"onboarding.addToDock.cta" = "Skip"; + +/* The message of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock. */ +"onboarding.addToDock.message" = "I can paddle into the Dock and perch there until you need me."; + +/* The title of the onboarding dialog popup informing the user on the benefits of adding the DDG browser icon to the dock. */ +"onboarding.addToDock.title" = "Want to add me to your Dock?"; + +/* Button on the Add to Dock tutorial screen of the onboarding, it will dismiss the screen and proceed to the next step. */ +"onboarding.addToDock.tutorial.cta" = "Got It"; + /* Button to change the default browser */ "onboarding.browsers.cta" = "Choose Your Browser"; diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index a8c3db67bf..585d936cd9 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -95,6 +95,6 @@ class AppSettingsMock: AppSettings { var newTabPageIntroMessageSeenCount: Int = 0 var onboardingHighlightsEnabled: Bool = false - var onboardingAddToDockEnabled: Bool = false - + var onboardingAddToDockState: OnboardingAddToDockState = .disabled + } diff --git a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift index 7f7665d7a6..a255eb4417 100644 --- a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift +++ b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift @@ -28,6 +28,7 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { private var delegate: ContextualOnboardingDelegateMock! private var settingsMock: ContextualOnboardingSettingsMock! private var pixelReporterMock: OnboardingPixelReporterMock! + private var onboardingManagerMock: OnboardingManagerMock! private var window: UIWindow! override func setUpWithError() throws { @@ -35,10 +36,12 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { delegate = ContextualOnboardingDelegateMock() settingsMock = ContextualOnboardingSettingsMock() pixelReporterMock = OnboardingPixelReporterMock() + onboardingManagerMock = OnboardingManagerMock() sut = ExperimentContextualDaxDialogsFactory( contextualOnboardingLogic: ContextualOnboardingLogicMock(), contextualOnboardingSettings: settingsMock, - contextualOnboardingPixelReporter: pixelReporterMock + contextualOnboardingPixelReporter: pixelReporterMock, + onboardingManager: onboardingManagerMock ) window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() @@ -50,6 +53,7 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { delegate = nil settingsMock = nil pixelReporterMock = nil + onboardingManagerMock = nil sut = nil try super.tearDownWithError() } @@ -335,6 +339,36 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { // THEN XCTAssertTrue(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) } + + // MARK: - Add To Dock + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenReturnExpectedCopy() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: delegate, onSizeUpdate: {}) + + // WHEN + let result = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // THEN + XCTAssertEqual(result.message, UserText.AddToDockOnboarding.EndOfJourney.message) + XCTAssertEqual(result.cta, UserText.AddToDockOnboarding.Buttons.dismiss) + } + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenCanShowAddToDockTutorialIsTrue() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = sut.makeView(for: spec, delegate: delegate, onSizeUpdate: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // WHEN + let result = view.canShowAddToDockTutorial + + // THEN + XCTAssertTrue(result) + } } extension ContextualDaxDialogsFactoryTests { diff --git a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift index 8d54d53d5a..aca4f086d2 100644 --- a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift +++ b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift @@ -29,6 +29,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { var mockDelegate: CapturingOnboardingNavigationDelegate! var contextualOnboardingLogicMock: ContextualOnboardingLogicMock! var pixelReporterMock: OnboardingPixelReporterMock! + var onboardingManagerMock: OnboardingManagerMock! var onDismissCalled: Bool! var window: UIWindow! @@ -36,9 +37,15 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { super.setUp() mockDelegate = CapturingOnboardingNavigationDelegate() contextualOnboardingLogicMock = ContextualOnboardingLogicMock() + onboardingManagerMock = OnboardingManagerMock() onDismissCalled = false pixelReporterMock = OnboardingPixelReporterMock() - factory = NewTabDaxDialogFactory(delegate: mockDelegate, contextualOnboardingLogic: contextualOnboardingLogicMock, onboardingPixelReporter: pixelReporterMock) + factory = NewTabDaxDialogFactory( + delegate: mockDelegate, + contextualOnboardingLogic: contextualOnboardingLogicMock, + onboardingPixelReporter: pixelReporterMock, + onboardingManager: onboardingManagerMock + ) window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() } @@ -51,6 +58,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { onDismissCalled = nil contextualOnboardingLogicMock = nil pixelReporterMock = nil + onboardingManagerMock = nil super.tearDown() } @@ -163,6 +171,36 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) } + // MARK: - Add To Dock + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenReturnExpectedCopy() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + + // WHEN + let result = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // THEN + XCTAssertEqual(result.message, UserText.AddToDockOnboarding.EndOfJourney.message) + XCTAssertEqual(result.cta, UserText.AddToDockOnboarding.Buttons.dismiss) + } + + func testWhenEndOfJourneyDialogAndAddToDockIsContextualThenCanShowAddToDockTutorialIsTrue() throws { + // GIVEN + let spec = DaxDialogs.HomeScreenSpec.final + onboardingManagerMock.addToDockEnabledState = .contextual + let dialog = factory.createDaxDialog(for: spec, onDismiss: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: dialog)) + + // WHEN + let result = view.canShowAddToDockTutorial + + // THEN + XCTAssertTrue(result) + } + } private extension ContextualOnboardingNewTabDialogFactoryTests { diff --git a/DuckDuckGoTests/DaxDialogTests.swift b/DuckDuckGoTests/DaxDialogTests.swift index 22bcfc1c7d..7a512f5cae 100644 --- a/DuckDuckGoTests/DaxDialogTests.swift +++ b/DuckDuckGoTests/DaxDialogTests.swift @@ -1100,6 +1100,52 @@ final class DaxDialog: XCTestCase { XCTAssertTrue(result) } + // MARK: - States + + func testWhenIsShowingAddToDockDialogCalledAndHomeSpecIsFinalAndAddToDockIsEnabledThenReturnTrue() { + // GIVEN + let onboardingManagerMock = OnboardingManagerMock() + onboardingManagerMock.addToDockEnabledState = .contextual + settings.fireMessageExperimentShown = true + let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + _ = sut.nextHomeScreenMessageNew() + + // WHEN + let result = sut.isShowingAddToDockDialog + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsShowingAddToDockDialogCalledAndHomeSpecIsNotFinalThenReturnFalse() { + // GIVEN + let onboardingManagerMock = OnboardingManagerMock() + onboardingManagerMock.addToDockEnabledState = .contextual + let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + _ = sut.nextHomeScreenMessageNew() + + // WHEN + let result = sut.isShowingAddToDockDialog + + // THEN + XCTAssertFalse(result) + } + + func testWhenIsShowingAddToDockDialogCalledAndHomeSpeciIsFinalAndAddToDockIsNotEnabledReturnFalse() { + // GIVEN + let onboardingManagerMock = OnboardingManagerMock() + onboardingManagerMock.addToDockEnabledState = .disabled + settings.fireMessageExperimentShown = true + let sut = makeExperimentSUT(settings: settings, onboardingManager: onboardingManagerMock) + _ = sut.nextHomeScreenMessageNew() + + // WHEN + let result = sut.isShowingAddToDockDialog + + // THEN + XCTAssertFalse(result) + } + private func detectedTrackerFrom(_ url: URL, pageUrl: String) -> DetectedRequest { let entity = entityProvider.entity(forHost: url.host!) return DetectedRequest(url: url.absoluteString, @@ -1123,11 +1169,11 @@ final class DaxDialog: XCTestCase { protectionStatus: protectionStatus) } - private func makeExperimentSUT(settings: DaxDialogsSettings) -> DaxDialogs { + private func makeExperimentSUT(settings: DaxDialogsSettings, onboardingManager: OnboardingAddToDockManaging = OnboardingManagerMock()) -> DaxDialogs { var mockVariantManager = MockVariantManager() mockVariantManager.isSupportedBlock = { feature in feature == .contextualDaxDialogs } - return DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) + return DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager, onboardingManager: onboardingManager) } } diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 2c8ee42d50..2637b72263 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -475,4 +475,34 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.title) } + // MARK: - Add To Dock + + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToAddToDockPromoDialogAndProgressIs2Of4() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .addToDockPromoDialog, step: .init(currentStep: 2, totalSteps: 4)))) + } + + func testWhenAddtoDockContinueActionIsCalledAndIsHighlightsIphoneFlowThenThenViewStateChangesToChooseAppIconAndProgressIs3of4() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + onboardingManager.addToDockEnabledState = .intro + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.addToDockContinueAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 3, totalSteps: 4)))) + } + } diff --git a/DuckDuckGoTests/OnboardingManagerMock.swift b/DuckDuckGoTests/OnboardingManagerMock.swift index 9322299bfe..dcd8473b60 100644 --- a/DuckDuckGoTests/OnboardingManagerMock.swift +++ b/DuckDuckGoTests/OnboardingManagerMock.swift @@ -20,6 +20,7 @@ import Foundation @testable import DuckDuckGo -final class OnboardingManagerMock: OnboardingHighlightsManaging { +final class OnboardingManagerMock: OnboardingHighlightsManaging, OnboardingAddToDockManaging { var isOnboardingHighlightsEnabled: Bool = false + var addToDockEnabledState: OnboardingAddToDockState = .disabled } diff --git a/DuckDuckGoTests/OnboardingManagerTests.swift b/DuckDuckGoTests/OnboardingManagerTests.swift index c5f0c2bff8..ef7d174fc0 100644 --- a/DuckDuckGoTests/OnboardingManagerTests.swift +++ b/DuckDuckGoTests/OnboardingManagerTests.swift @@ -155,26 +155,37 @@ final class OnboardingManagerTests: XCTestCase { // MARK: - Add to Dock - func testWhenIsAddToDockLocalFlagEnabledCalledAndAppDefaultsOnboardingAddToDockEnabledIsTrueThenReturnTrue() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsIntroThenReturnIntro() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro // WHEN - let result = sut.isAddToDockLocalFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertTrue(result) + XCTAssertEqual(result, .intro) } - func testWhenIsAddToDockLocalFlagEnabledCalledAndAppDefaultsOnboardingAddToDockEnabledIsFalseThenReturnFalse() { + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsContextualThenReturnContextual() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = false + appSettingsMock.onboardingAddToDockState = .contextual // WHEN - let result = sut.isAddToDockLocalFlagEnabled + let result = sut.addToDockLocalFlagState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .contextual) + } + + func testWhenAddToDockLocalFlagStateCalledAndAppDefaultsOnboardingAddToDockStateIsDisabledThenReturnDisabled() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .disabled + + // WHEN + let result = sut.addToDockLocalFlagState + + // THEN + XCTAssertEqual(result, .disabled) } func testWhenIsAddToDockFeatureFlagEnabledCalledAndFeaturFlaggerFeatureIsOnThenReturnTrue() { @@ -199,65 +210,102 @@ final class OnboardingManagerTests: XCTestCase { XCTAssertFalse(result) } - func testWhenIsAddToDockEnabledCalledAndLocalFlagEnabledIsFalseAndFeatureFlagIsFalseThenReturnFalse() { + func testWhenAddToDockStateCalledAndLocalFlagStateIsDisabledAndFeatureFlagIsFalseThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = false + appSettingsMock.onboardingAddToDockState = .disabled featureFlaggerMock.enabledFeatureFlags = [] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockEnabledCalledAndLocalFlagEnabledIsTrueAndFeatureFlagIsFalseThenReturnFalse() { + func testWhenAddToDockStateCalledAndLocalFlagStateIsIntroAndFeatureFlagIsFalseThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro featureFlaggerMock.enabledFeatureFlags = [] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) + } + + func testWhenAddToDockStateCalledAndLocalFlagStateIsContextualAndFeatureFlagIsFalseThenReturnDisabled() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .contextual + featureFlaggerMock.enabledFeatureFlags = [] + + // WHEN + let result = sut.addToDockEnabledState + + // THEN + XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockEnabledCalledAndLocalFlagEnabledIsFalseAndFeatureFlagEnabledIsTrueThenReturnFalse() { + func testWhenAddToDockStateCalledAndLocalFlagStateIsDisabledAndFeatureFlagEnabledIsTrueThenReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = false + appSettingsMock.onboardingAddToDockState = .disabled featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) } - func testWhenIsAddToDockEnabledAndLocalFlagEnabledIsTrueAndFeatureFlagEnabledIsTrueThenReturnTrue() { + func testWhenAddToDockStateAndLocalFlagStateIsIntroAndFeatureFlagEnabledIsTrueThenReturnIntro() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertTrue(result) + XCTAssertEqual(result, .intro) + } + + func testWhenAddToDockStateAndLocalFlagStateIsContextualAndFeatureFlagEnabledIsTrueThenReturnContextual() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .contextual + featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] + + // WHEN + let result = sut.addToDockEnabledState + + // THEN + XCTAssertEqual(result, .contextual) } - func testWhenIsAddToDockEnabledAndLocalAndFeatureFlagsAreEnabledAndDeviceIsIpadReturnFalse() { + func testWhenAddToDockStateAndLocalFlagStateIsIntroAndFeatureFlagsIsEnabledAndDeviceIsIpadReturnDisabled() { // GIVEN - appSettingsMock.onboardingAddToDockEnabled = true + appSettingsMock.onboardingAddToDockState = .intro featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock, isIphone: false) // WHEN - let result = sut.isAddToDockEnabled + let result = sut.addToDockEnabledState // THEN - XCTAssertFalse(result) + XCTAssertEqual(result, .disabled) + } + + func testWhenAddToDockStateAndLocalFlagStateIsContextualAndFeatureFlagsIsEnabledAndDeviceIsIpadReturnDisabled() { + // GIVEN + appSettingsMock.onboardingAddToDockState = .contextual + featureFlaggerMock.enabledFeatureFlags = [.onboardingAddToDock] + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock, isIphone: false) + + // WHEN + let result = sut.addToDockEnabledState + + // THEN + XCTAssertEqual(result, .disabled) } } diff --git a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift index 5bafc76247..1e22244892 100644 --- a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift @@ -242,6 +242,7 @@ final class ContextualOnboardingLogicMock: ContextualOnboardingLogic { var shouldShowPrivacyButtonPulse: Bool = false var isShowingSearchSuggestions: Bool = false var isShowingSitesSuggestions: Bool = false + var isShowingAddToDockDialog: Bool = false func setFireEducationMessageSeen() { didCallSetFireEducationMessageSeen = true From d9a02a30214a831491d60199d1e099dd5b95904f Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 5 Nov 2024 18:45:09 +0100 Subject: [PATCH 09/10] Send pixel on sync secure storage failure (#3542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414235014887631/1208700858621924/f **Description**: This was reviewed here: https://github.com/duckduckgo/iOS/pull/3530 and merged already to the release branch, but there's been conflicts since. So it needs an extra PR to resolve them. **Steps to test this PR**: 1. Just make sure this compiles and is pointing to latest BSK release. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --------- Co-authored-by: Daniel Bernal --- Core/PixelEvent.swift | 2 ++ Core/SyncErrorHandler.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index ed52ce6a71..869d390f13 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -624,6 +624,7 @@ extension Pixel { case syncRemoveDeviceError case syncDeleteAccountError case syncLoginExistingAccountError + case syncSecureStorageReadError case syncGetOtherDevices case syncGetOtherDevicesCopy @@ -1437,6 +1438,7 @@ extension Pixel.Event { case .syncRemoveDeviceError: return "m_d_sync_remove_device_error" case .syncDeleteAccountError: return "m_d_sync_delete_account_error" case .syncLoginExistingAccountError: return "m_d_sync_login_existing_account_error" + case .syncSecureStorageReadError: return "m_d_sync_secure_storage_error" case .syncGetOtherDevices: return "sync_get_other_devices" case .syncGetOtherDevicesCopy: return "sync_get_other_devices_copy" diff --git a/Core/SyncErrorHandler.swift b/Core/SyncErrorHandler.swift index a3ff07e794..93609732ba 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -100,6 +100,8 @@ public class SyncErrorHandler: EventMapping { Pixel.fire(pixel: .syncFailedToLoadAccount, error: error) case .failedToSetupEngine: Pixel.fire(pixel: .syncFailedToSetupEngine, error: error) + case .failedToReadSecureStore: + Pixel.fire(pixel: .syncSecureStorageReadError, error: error) default: // Should this be so generic? let domainEvent = Pixel.Event.syncSentUnauthenticatedRequest From 779b5bbd8fef04a8bf4ad09657025007f788360b Mon Sep 17 00:00:00 2001 From: Shilpa Modi Date: Tue, 5 Nov 2024 11:05:02 -0800 Subject: [PATCH 10/10] Adding app backgrounded result to rule compilation (#3533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204556816597738/1208691511506504/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. 2. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/PixelEvent.swift | 1 + DuckDuckGo/RulesCompilationMonitor.swift | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 869d390f13..415cd2f1b4 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -1729,6 +1729,7 @@ extension Pixel.Event { case tabClosed = "tab_closed" case appQuit = "app_quit" + case appBackgrounded = "app_backgrounded" case success } diff --git a/DuckDuckGo/RulesCompilationMonitor.swift b/DuckDuckGo/RulesCompilationMonitor.swift index 1282bff99f..eea332470d 100644 --- a/DuckDuckGo/RulesCompilationMonitor.swift +++ b/DuckDuckGo/RulesCompilationMonitor.swift @@ -41,6 +41,11 @@ final class RulesCompilationMonitor { selector: #selector(applicationWillTerminate(_:)), name: UIApplication.willTerminateNotification, object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil) } /// Called when a Tab is going to wait for Content Blocking Rules compilation @@ -88,6 +93,18 @@ final class RulesCompilationMonitor { reportWaitTime(CACurrentMediaTime() - waitStart, result: .appQuit) } + + /// If App is going into the background while the rules are still being compiled, report the time so that we + /// do not continue to count the time in background + @objc func applicationDidEnterBackground() { + guard !didReport, + !waiters.isEmpty, + let waitStart = waitStart + else { return } + + reportWaitTime(CACurrentMediaTime() - waitStart, result: .appBackgrounded) + } + private func reportWaitTime(_ waitTime: TimeInterval, result: Pixel.Event.CompileRulesResult) { didReport = true