From 9ed705c9641d1d16d9477494b15c34c568622d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 16 Sep 2024 12:40:14 +0200 Subject: [PATCH 01/46] New Tab Page post-review fixes (#3221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1208023858138598/f Tech Design URL: CC: **Description**: This PR contains fixes for issues and glitches found for New Tab Page Improvements. **Steps to test this PR**: No specific steps but rather a set of things that were fixed/modified. Overall feature smoke check is required. 1. Missing translations for options in NTP settings. 2. Favorites blinking when NTP becomes visible. 3. Display issues on reordering shortcuts in settings. 4. Animations glitches for expand button and favorites/shortcuts collections. 5. Customize button placement based on the contents. 6. NTP content margins and layout. 7. Spacings and list separators padding in NTP Settings. 8. Add haptics on items reordering. 9. Dark mode color for section settings icon. 10. Color adjustments for shortcuts in edit mode. 11. NTP empty state layout. 12. Grid layout based on orientation. 13. Grid padding / item spacing. 14. A few code-related improvements. For a full list and details see completed subtasks of https://app.asana.com/0/72649045549333/1207539163549343/f. **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) --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../24px/Shortcut-24.imageset/Contents.json | 3 +- DuckDuckGo/EditableShortcutsView.swift | 21 +- DuckDuckGo/FaviconsHelper.swift | 65 ++--- DuckDuckGo/Favorite.swift | 3 +- DuckDuckGo/FavoriteIconView.swift | 8 +- DuckDuckGo/FavoritesDefaultModel.swift | 48 +++- DuckDuckGo/FavoritesEmptyStateView.swift | 39 +-- DuckDuckGo/FavoritesFaviconLoader.swift | 45 ++-- DuckDuckGo/FavoritesPreviewModel.swift | 4 + DuckDuckGo/FavoritesSectionHeader.swift | 2 +- DuckDuckGo/FavoritesView.swift | 14 +- DuckDuckGo/MainViewController.swift | 7 +- DuckDuckGo/NewTabPageGridView.swift | 41 ++- DuckDuckGo/NewTabPageIntroMessageView.swift | 2 +- DuckDuckGo/NewTabPageManager.swift | 11 +- DuckDuckGo/NewTabPageSectionsDebugView.swift | 2 +- .../NewTabPageSettingsSectionItemView.swift | 4 +- DuckDuckGo/NewTabPageSettingsView.swift | 19 +- DuckDuckGo/NewTabPageShortcut.swift | 4 + .../NewTabPageShortcutsSettingsStorage.swift | 2 +- DuckDuckGo/NewTabPageView.swift | 253 ++++++++++++------ DuckDuckGo/NewTabPageViewController.swift | 5 +- DuckDuckGo/ReorderableForEach.swift | 29 +- DuckDuckGo/ShortcutAccessoryView.swift | 35 +-- DuckDuckGo/ShortcutItemView.swift | 48 ++-- DuckDuckGo/ShortcutsView.swift | 5 +- ...ew.swift => ToggleExpandButtonStyle.swift} | 39 +-- DuckDuckGo/UserText.swift | 6 +- DuckDuckGo/ViewExtension.swift | 3 +- DuckDuckGo/bg.lproj/Localizable.strings | 27 +- DuckDuckGo/cs.lproj/Localizable.strings | 29 +- DuckDuckGo/da.lproj/Localizable.strings | 29 +- DuckDuckGo/de.lproj/Localizable.strings | 27 +- DuckDuckGo/el.lproj/Localizable.strings | 29 +- DuckDuckGo/en.lproj/Localizable.strings | 11 +- DuckDuckGo/es.lproj/Localizable.strings | 29 +- DuckDuckGo/et.lproj/Localizable.strings | 29 +- DuckDuckGo/fi.lproj/Localizable.strings | 29 +- DuckDuckGo/fr.lproj/Localizable.strings | 29 +- DuckDuckGo/hr.lproj/Localizable.strings | 29 +- DuckDuckGo/hu.lproj/Localizable.strings | 27 +- DuckDuckGo/it.lproj/Localizable.strings | 29 +- DuckDuckGo/lt.lproj/Localizable.strings | 29 +- DuckDuckGo/lv.lproj/Localizable.strings | 29 +- DuckDuckGo/nb.lproj/Localizable.strings | 29 +- DuckDuckGo/nl.lproj/Localizable.strings | 27 +- DuckDuckGo/pl.lproj/Localizable.strings | 29 +- DuckDuckGo/pt.lproj/Localizable.strings | 29 +- DuckDuckGo/ro.lproj/Localizable.strings | 29 +- DuckDuckGo/ru.lproj/Localizable.strings | 29 +- DuckDuckGo/sk.lproj/Localizable.strings | 29 +- DuckDuckGo/sl.lproj/Localizable.strings | 29 +- DuckDuckGo/sv.lproj/Localizable.strings | 29 +- DuckDuckGo/tr.lproj/Localizable.strings | 29 +- .../NewTabPageFavoritesModelTests.swift | 1 + 56 files changed, 809 insertions(+), 666 deletions(-) rename DuckDuckGo/{ToggleExpandButtonView.swift => ToggleExpandButtonStyle.swift} (65%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 632258f36e..23688459f1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -294,7 +294,7 @@ 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */; }; - 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */; }; + 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */; }; 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; }; 6F64AA592C4818D700CF4489 /* NewTabPageShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */; }; 6F64AA5B2C481AAA00CF4489 /* Shortcuts.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */; }; @@ -1562,7 +1562,7 @@ 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorage.swift; sourceTree = ""; }; - 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonView.swift; sourceTree = ""; }; + 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonStyle.swift; sourceTree = ""; }; 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = ""; }; 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcut.swift; sourceTree = ""; }; 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Shortcuts.xcassets; sourceTree = ""; }; @@ -3938,7 +3938,7 @@ 6F35379C2C4AAF1C009F8717 /* Settings */, 6FE127472C20941A00EB5724 /* Shortcuts */, 6FE127412C204DE900EB5724 /* Favorites */, - 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */, + 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */, 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */, 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */, 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */, @@ -7495,7 +7495,7 @@ 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, - 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonView.swift in Sources */, + 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */, D68A21462B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift in Sources */, 31DE43C22C2C480D00F8C51F /* DuckPlayerFeaturePresentationView.swift in Sources */, F4C9FBF528340DDA002281CC /* AutofillInterfaceEmailTruncator.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json index 74079951c3..0d82d94b3b 100644 --- a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" } } diff --git a/DuckDuckGo/EditableShortcutsView.swift b/DuckDuckGo/EditableShortcutsView.swift index f907a06d5b..1ceca96f54 100644 --- a/DuckDuckGo/EditableShortcutsView.swift +++ b/DuckDuckGo/EditableShortcutsView.swift @@ -23,19 +23,28 @@ import UniformTypeIdentifiers struct EditableShortcutsView: View { @ObservedObject private(set) var model: NewTabPageShortcutsSettingsModel + let geometry: GeometryProxy? + + private let haptics = UIImpactFeedbackGenerator() var body: some View { - NewTabPageGridView { _ in + NewTabPageGridView(geometry: geometry) { _ in ReorderableForEach(model.itemsSettings, id: \.item.id, isReorderingEnabled: true) { setting in let isEnabled = model.enabledItems.contains(setting.item) Button { setting.isEnabled.wrappedValue.toggle() } label: { ShortcutItemView(shortcut: setting.item, accessoryType: isEnabled ? .selected : .add) + .frame(width: NewTabPageGrid.Item.edgeSize) } + .padding([.horizontal, .top], 6) // Adjust for the accessory being cut-off when lifting for preview + .previewShape() } preview: { setting in - ShortcutIconView(shortcut: setting.item).previewShape() + ShortcutIconView(shortcut: setting.item) + .previewShape() + .frame(width: NewTabPageGrid.Item.edgeSize) } onMove: { indices, newOffset in + haptics.impactOccurred() withAnimation { model.moveItems(from: indices, to: newOffset) } @@ -46,7 +55,7 @@ struct EditableShortcutsView: View { private extension View { func previewShape() -> some View { - contentShape(.dragPreview, RoundedRectangle(cornerRadius: 8)) + contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 8)) } } @@ -68,8 +77,10 @@ extension NewTabPageSettingsModel.NTPSetting: Reorderable, H } #Preview { - ScrollView { - EditableShortcutsView(model: NewTabPageShortcutsSettingsModel()) + GeometryReader { proxy in + ScrollView { + EditableShortcutsView(model: NewTabPageShortcutsSettingsModel(), geometry: proxy) + } } .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/FaviconsHelper.swift b/DuckDuckGo/FaviconsHelper.swift index 4f64181e46..051acccb6a 100644 --- a/DuckDuckGo/FaviconsHelper.swift +++ b/DuckDuckGo/FaviconsHelper.swift @@ -24,15 +24,14 @@ import Common struct FaviconsHelper { - private static let tld: TLD = AppDependencyProvider.shared.storageCache.tld + private static let tld: TLD = AppDependencyProvider.shared.storageCache.tld static func loadFaviconSync(forDomain domain: String?, usingCache cacheType: Favicons.CacheType, useFakeFavicon: Bool, - preferredFakeFaviconLetters: String? = nil, - completion: ((UIImage?, Bool) -> Void)? = nil) { - - func complete(_ image: UIImage?) { + preferredFakeFaviconLetters: String? = nil) -> (image: UIImage?, isFake: Bool) { + + func complete(_ image: UIImage?) -> (UIImage?, Bool) { var fake = false var resultImage: UIImage? @@ -44,72 +43,60 @@ struct FaviconsHelper { backgroundColor: UIColor.forDomain(domain), preferredFakeFaviconLetters: preferredFakeFaviconLetters) } - completion?(resultImage, fake) + return (resultImage, fake) } if domain == "player" { - complete(UIImage(named: "DuckPlayer")) - return + return complete(UIImage(named: "DuckPlayer")) } if URL.isDuckDuckGo(domain: domain) { - complete(UIImage(named: "Logo")) - return + return complete(UIImage(named: "Logo")) } guard let cache = Favicons.Constants.caches[cacheType] else { - complete(nil) - return + return complete(nil) } guard let resource = Favicons.shared.defaultResource(forDomain: domain) else { - complete(nil) - return + return complete(nil) } if let image = cache.retrieveImageInMemoryCache(forKey: resource.cacheKey) { - complete(image) + return complete(image) } else { // Load manually otherwise Kingfisher won't load it if the file's modification date > current date let url = cache.diskStorage.cacheFileURL(forKey: resource.cacheKey) guard let data = (try? Data(contentsOf: url)), let image = UIImage(data: data) else { - complete(nil) - return + return complete(nil) } - - complete(image) // Cache in memory with the original expiry date so that the image will be refreshed on user interaction. - guard let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)), - let fileModificationDate = attributes[.modificationDate] as? Date else { - return + if let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)), + let fileModificationDate = attributes[.modificationDate] as? Date { + + cache.store(image, forKey: resource.cacheKey, options: KingfisherParsedOptionsInfo([ + .cacheMemoryOnly, + .diskCacheAccessExtendingExpiration(.none), + .memoryCacheExpiration(.date(fileModificationDate)) + ]), toDisk: false) } - - cache.store(image, forKey: resource.cacheKey, options: KingfisherParsedOptionsInfo([ - .cacheMemoryOnly, - .diskCacheAccessExtendingExpiration(.none), - .memoryCacheExpiration(.date(fileModificationDate)) - ]), toDisk: false) - + + return complete(image) } } - @MainActor static func loadFaviconSync(forDomain domain: String?, usingCache cacheType: Favicons.CacheType, useFakeFavicon: Bool, - preferredFakeFaviconLetters: String? = nil) async -> (image: UIImage?, isFake: Bool) { - await withCheckedContinuation { continuation in - loadFaviconSync(forDomain: domain, - usingCache: cacheType, - useFakeFavicon: useFakeFavicon, - preferredFakeFaviconLetters: preferredFakeFaviconLetters) { image, isFake in - continuation.resume(returning: (image, isFake)) - } - } + preferredFakeFaviconLetters: String? = nil, + completion: ((UIImage?, Bool) -> Void)? = nil) { + let result = loadFaviconSync(forDomain: domain, usingCache: cacheType, useFakeFavicon: useFakeFavicon, preferredFakeFaviconLetters: preferredFakeFaviconLetters) + + completion?(result.image, result.isFake) } static func createFakeFavicon(forDomain domain: String, diff --git a/DuckDuckGo/Favorite.swift b/DuckDuckGo/Favorite.swift index 5b13b1f484..29d5eb70da 100644 --- a/DuckDuckGo/Favorite.swift +++ b/DuckDuckGo/Favorite.swift @@ -38,8 +38,9 @@ struct Favorite: Identifiable, Equatable, Hashable { struct Favicon: Equatable, Hashable { let image: UIImage let isUsingBorder: Bool + let isFake: Bool - static let empty = Self.init(image: UIImage(), isUsingBorder: false) + static let empty = Self.init(image: UIImage(), isUsingBorder: false, isFake: true) } extension Favorite { diff --git a/DuckDuckGo/FavoriteIconView.swift b/DuckDuckGo/FavoriteIconView.swift index 88f4351280..a640fcf95e 100644 --- a/DuckDuckGo/FavoriteIconView.swift +++ b/DuckDuckGo/FavoriteIconView.swift @@ -22,6 +22,8 @@ import SwiftUI protocol FavoritesFaviconLoading { func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon + + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? } struct FavoriteIconView: View { @@ -46,7 +48,7 @@ struct FavoriteIconView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } .task { - if let favicon = await faviconLoading?.loadFavicon(for: favorite, size: Constant.faviconSize) { + if favicon.isFake, let favicon = await faviconLoading?.loadFavicon(for: favorite, size: Constant.faviconSize) { self.favicon = favicon } } @@ -74,7 +76,9 @@ private extension Favorite { extension FavoriteIconView { init(favorite: Favorite, faviconLoading: FavoritesFaviconLoading? = nil) { - let favicon = faviconLoading?.fakeFavicon(for: favorite, size: Constant.faviconSize) ?? .empty + let favicon = faviconLoading?.existingFavicon(for: favorite, size: Constant.faviconSize) + ?? faviconLoading?.fakeFavicon(for: favorite, size: Constant.faviconSize) + ?? .empty self.init(favicon: favicon, favorite: favorite, faviconLoading: faviconLoading) } } diff --git a/DuckDuckGo/FavoritesDefaultModel.swift b/DuckDuckGo/FavoritesDefaultModel.swift index 794bb34d1a..92cf6e3ab3 100644 --- a/DuckDuckGo/FavoritesDefaultModel.swift +++ b/DuckDuckGo/FavoritesDefaultModel.swift @@ -30,15 +30,7 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { @Published private(set) var isCollapsed: Bool = true @Published private(set) var isShowingTooltip: Bool = false - private(set) lazy var faviconLoader: FavoritesFaviconLoading? = { - FavoritesFaviconLoader(onFaviconMissing: { [weak self] in - guard let self else { return } - - await MainActor.run { - self.faviconMissing() - } - }) - }() + private(set) var faviconLoader: FavoritesFaviconLoading? private var cancellables = Set() @@ -51,11 +43,20 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { } init(interactionModel: FavoritesListInteracting, + faviconLoader: FavoritesFaviconLoading, pixelFiring: PixelFiring.Type = Pixel.self, dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { self.interactionModel = interactionModel self.pixelFiring = pixelFiring self.dailyPixelFiring = dailyPixelFiring + self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in + guard let self else { return } + + await MainActor.run { + self.faviconMissing() + } + }) + interactionModel.externalUpdates.sink { [weak self] _ in try? self?.updateData() @@ -203,3 +204,32 @@ private extension BookmarkEntity { } } + +private final class MissingFaviconWrapper: FavoritesFaviconLoading { + let loader: FavoritesFaviconLoading + + private(set) var onFaviconMissing: (() async -> Void) + + init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { + self.onFaviconMissing = onFaviconMissing + self.loader = loader + } + + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + let favicon = await loader.loadFavicon(for: favorite, size: size) + + if favicon == nil { + await onFaviconMissing() + } + + return favicon + } + + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + loader.fakeFavicon(for: favorite, size: size) + } + + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + loader.existingFavicon(for: favorite, size: size) + } +} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift index 60d528104c..15db8ab2ca 100644 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ b/DuckDuckGo/FavoritesEmptyStateView.swift @@ -25,15 +25,14 @@ struct FavoritesEmptyStateView: View { @ObservedObject var model: Model - @State private var headerPadding: CGFloat = 10 + let geometry: GeometryProxy? var body: some View { ZStack(alignment: .topTrailing) { VStack(spacing: 16) { FavoritesSectionHeader(model: model) - .padding(.horizontal, headerPadding) - NewTabPageGridView { placeholdersCount in + NewTabPageGridView(geometry: geometry) { placeholdersCount in let placeholders = Array(0..: View { model.placeholderTapped() } } - }.overlay( - GeometryReader(content: { geometry in - Color.clear.preference(key: WidthKey.self, value: geometry.frame(in: .local).width) - }) - ) - .onPreferenceChange(WidthKey.self, perform: { fullWidth in - let columnsCount = Double(NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape)) - let allColumnsWidth = columnsCount * NewTabPageGrid.Item.edgeSize - let leftoverWidth = fullWidth - allColumnsWidth - let spacingSize = leftoverWidth / (columnsCount) - self.headerPadding = spacingSize / 2 - }) + } } if model.isShowingTooltip { FavoritesTooltip() - .offset(x: -headerPadding + 18, y: 24) + .offset(x: 18, y: 24) .frame(maxWidth: .infinity, alignment: .bottomTrailing) } } @@ -67,22 +55,5 @@ struct FavoritesEmptyStateView: View { } #Preview { - return FavoritesEmptyStateView(model: FavoritesPreviewModel()) -} - -private struct WidthKey: PreferenceKey { - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } - static var defaultValue: CGFloat = .zero -} - -private final class PreviewEmptyStateModel: FavoritesEmptyStateModel { - @Published var isShowingTooltip: Bool = true - - func toggleTooltip() { - } - - func placeholderTapped() { - } + return FavoritesEmptyStateView(model: FavoritesPreviewModel(), geometry: nil) } diff --git a/DuckDuckGo/FavoritesFaviconLoader.swift b/DuckDuckGo/FavoritesFaviconLoader.swift index 874ff59421..7adc516ce9 100644 --- a/DuckDuckGo/FavoritesFaviconLoader.swift +++ b/DuckDuckGo/FavoritesFaviconLoader.swift @@ -20,41 +20,35 @@ import UIKit actor FavoritesFaviconLoader: FavoritesFaviconLoading { - private var tasks: [URL: Task] = [:] - private(set) var onFaviconMissing: (() async -> Void)? - - init(onFaviconMissing: (() async -> Void)? = nil) { - self.onFaviconMissing = onFaviconMissing - } + + private var tasks: [String: Task] = [:] func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { - guard let url = favorite.urlObject else { return nil } + let domain = favorite.domain - if let task = tasks[url] { + if let task = tasks[domain] { if task.isCancelled { - tasks.removeValue(forKey: url) + tasks.removeValue(forKey: domain) } else { return await task.value } } let newTask = Task { - let faviconResult = await FaviconsHelper.loadFaviconSync(forDomain: favorite.domain, usingCache: .fireproof, useFakeFavicon: false) - if let iconImage = faviconResult.image { - let useBorder = URL.isDuckDuckGo(domain: favorite.domain) || iconImage.size.width < size - - return Favicon(image: iconImage, isUsingBorder: useBorder) - } else { - await onFaviconMissing?() - return nil - } + let faviconResult = FaviconsHelper.loadFaviconSync(forDomain: domain, usingCache: .fireproof, useFakeFavicon: false) + return Favicon(domain: domain, expectedSize: size, faviconResult: faviconResult) } - tasks[url] = newTask + tasks[domain] = newTask return await newTask.value } + nonisolated func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + let result = FaviconsHelper.loadFaviconSync(forDomain: favorite.domain, usingCache: .fireproof, useFakeFavicon: false) + return Favicon(domain: favorite.domain, expectedSize: size, faviconResult: result) + } + nonisolated func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { let domain = favorite.domain let color = UIColor.forDomain(domain) @@ -66,9 +60,20 @@ actor FavoritesFaviconLoader: FavoritesFaviconLoading { ) if let icon { - return Favicon(image: icon, isUsingBorder: false) + return Favicon(image: icon, isUsingBorder: false, isFake: true) } else { return .empty } } } + +private extension Favicon { + init?(domain: String, expectedSize: CGFloat, faviconResult: (image: UIImage?, isFake: Bool)) { + guard let iconImage = faviconResult.image else { + return nil + } + + let useBorder = URL.isDuckDuckGo(domain: domain) || iconImage.size.width < expectedSize + self.init(image: iconImage, isUsingBorder: useBorder, isFake: faviconResult.isFake) + } +} diff --git a/DuckDuckGo/FavoritesPreviewModel.swift b/DuckDuckGo/FavoritesPreviewModel.swift index fc17ec9c60..4f2f9eb99a 100644 --- a/DuckDuckGo/FavoritesPreviewModel.swift +++ b/DuckDuckGo/FavoritesPreviewModel.swift @@ -91,6 +91,10 @@ final class FavoritesPreviewModel: FavoritesModel, FavoritesEmptyStateModel { } struct EmptyFaviconLoading: FavoritesFaviconLoading { + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + nil + } + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { .empty } diff --git a/DuckDuckGo/FavoritesSectionHeader.swift b/DuckDuckGo/FavoritesSectionHeader.swift index 10a0779460..4a9d744e74 100644 --- a/DuckDuckGo/FavoritesSectionHeader.swift +++ b/DuckDuckGo/FavoritesSectionHeader.swift @@ -27,7 +27,7 @@ struct FavoritesSectionHeader: View { var body: some View { HStack(spacing: 16, content: { - Text("Favorites") + Text(UserText.newTabPageFavoritesSectionHeaderTitle) .font(.system(size: 15, weight: .semibold)) .foregroundColor(Color(designSystemColor: .textPrimary)) .frame(alignment: .leading) diff --git a/DuckDuckGo/FavoritesView.swift b/DuckDuckGo/FavoritesView.swift index bd2f690fe5..d5fcc55d9a 100644 --- a/DuckDuckGo/FavoritesView.swift +++ b/DuckDuckGo/FavoritesView.swift @@ -26,8 +26,10 @@ struct FavoritesView: View { @Environment(\.isLandscapeOrientation) var isLandscape @ObservedObject var model: Model + let geometry: GeometryProxy? private let selectionFeedback = UISelectionFeedbackGenerator() + private let haptics = UIImpactFeedbackGenerator() var body: some View { VStack(alignment: .center, spacing: 24) { @@ -35,7 +37,7 @@ struct FavoritesView: View { let columns = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape) let result = model.prefixedFavorites(for: columns) - NewTabPageGridView { _ in + NewTabPageGridView(geometry: geometry) { _ in ReorderableForEach(result.items) { item in Button(action: { model.favoriteSelected(item) @@ -54,11 +56,14 @@ struct FavoritesView: View { .frame(width: NewTabPageGrid.Item.edgeSize) }) .previewShape() + .transition(.opacity) } preview: { favorite in FavoriteIconView(favorite: favorite, faviconLoading: model.faviconLoader) .frame(width: NewTabPageGrid.Item.edgeSize) .previewShape() + .transition(.opacity) } onMove: { from, to in + haptics.impactOccurred() withAnimation { model.moveFavorites(from: from, to: to) } @@ -75,8 +80,13 @@ struct FavoritesView: View { .resizable() }) .buttonStyle(ToggleExpandButtonStyle()) + // Masks the content, which will otherwise shop up underneath while collapsing + .background(Color(designSystemColor: .background)) } } + // Prevent the content to leak out of bounds while collapsing + .clipped() + .padding(0) } } @@ -97,5 +107,5 @@ extension Favorite: Reorderable { } #Preview { - FavoritesView(model: FavoritesPreviewModel()) + FavoritesView(model: FavoritesPreviewModel(), geometry: nil) } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index edf27a7393..12e5ae1e7b 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -128,7 +128,8 @@ class MainViewController: UIViewController { let privacyProDataReporter: PrivacyProDataReporting private lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger - + private lazy var faviconLoader: FavoritesFaviconLoading = FavoritesFaviconLoader() + lazy var menuBookmarksViewModel: MenuBookmarksInteracting = { let viewModel = MenuBookmarksViewModel(bookmarksDatabase: bookmarksDatabase, syncService: syncService) viewModel.favoritesDisplayMode = appSettings.favoritesDisplayMode @@ -633,6 +634,7 @@ class MainViewController: UIViewController { UIView.animate(withDuration: duration, delay: 0, options: animationCurve) { self.viewCoordinator.navigationBarContainer.superview?.layoutIfNeeded() + self.newTabPageViewController?.additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: max(52, keyboardHeight), right: 0) } } @@ -794,7 +796,8 @@ class MainViewController: UIViewController { privacyProDataReporting: privacyProDataReporter, variantManager: variantManager, newTabDialogFactory: newTabDaxDialogFactory, - newTabDialogTypeProvider: DaxDialogs.shared) + newTabDialogTypeProvider: DaxDialogs.shared, + faviconLoader: faviconLoader) controller.delegate = self controller.shortcutsDelegate = self diff --git a/DuckDuckGo/NewTabPageGridView.swift b/DuckDuckGo/NewTabPageGridView.swift index 9f87036355..f2b75d76b5 100644 --- a/DuckDuckGo/NewTabPageGridView.swift +++ b/DuckDuckGo/NewTabPageGridView.swift @@ -22,21 +22,52 @@ import SwiftUI struct NewTabPageGridView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.isLandscapeOrientation) var isLandscape - + + let geometry: GeometryProxy? @ViewBuilder var content: (_ columnsCount: Int) -> Content + @State private var width: CGFloat = .zero + var body: some View { let columnsCount = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape) - LazyVGrid(columns: flexibleColumns(columnsCount), spacing: 24, content: { + LazyVGrid(columns: flexibleColumns(columnsCount, width: width), spacing: 24, content: { content(columnsCount) }) + .frame(maxWidth: .infinity) + .anchorPreference(key: FramePreferenceKey.self, value: .bounds, transform: { anchor in + guard let geometry else { return FramePreferenceKey.defaultValue } + + return geometry[anchor].width + }) + .onPreferenceChange(FramePreferenceKey.self, perform: { value in + width = value + }) .padding(0) - .offset(.zero) } - private func flexibleColumns(_ count: Int) -> [GridItem] { - Array(repeating: GridItem(.flexible(minimum: NewTabPageGrid.Item.edgeSize), alignment: .top), count: count) + private func flexibleColumns(_ count: Int, width: CGFloat) -> [GridItem] { + let spacing: CGFloat? + if width != .zero { + let columnsWidth = NewTabPageGrid.Item.edgeSize * Double(count) + let spacingsCount = count - 1 + // Calculate exact spacing so that there's no leading and trailing padding. + spacing = max((width - columnsWidth) / Double(spacingsCount), 0) + } else { + spacing = nil + } + + return Array(repeating: GridItem(.flexible(), + spacing: spacing, + alignment: .top), + count: count) + } +} + +private struct FramePreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() } } diff --git a/DuckDuckGo/NewTabPageIntroMessageView.swift b/DuckDuckGo/NewTabPageIntroMessageView.swift index c5682b9f54..f140a9ee72 100644 --- a/DuckDuckGo/NewTabPageIntroMessageView.swift +++ b/DuckDuckGo/NewTabPageIntroMessageView.swift @@ -62,7 +62,7 @@ struct NewTabPageIntroMessageView: View { private enum Metrics { static let padding = 8.0 static let itemSpacing = 6.0 - static let cornerRadius = 12.0 + static let cornerRadius = 8.0 static let titlePadding = 20.0 } } diff --git a/DuckDuckGo/NewTabPageManager.swift b/DuckDuckGo/NewTabPageManager.swift index 9723dd37d3..6a7dfa2baa 100644 --- a/DuckDuckGo/NewTabPageManager.swift +++ b/DuckDuckGo/NewTabPageManager.swift @@ -35,18 +35,23 @@ final class NewTabPageManager: NewTabPageManaging, NewTabPageDebugging { var appDefaults: AppDebugSettings let featureFlagger: FeatureFlagger + let internalUserDecider: InternalUserDecider init(appDefaults: AppDebugSettings = AppDependencyProvider.shared.appSettings, - featureFlager: FeatureFlagger = AppDependencyProvider.shared.featureFlagger) { - + featureFlager: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + internalUserDecider: InternalUserDecider = AppDependencyProvider.shared.internalUserDecider) { + self.appDefaults = appDefaults self.featureFlagger = featureFlager + self.internalUserDecider = internalUserDecider } // MARK: - HomeTabManaging var isNewTabPageSectionsEnabled: Bool { - isLocalFlagEnabled && isFeatureFlagEnabled + let isLocalFlagInEffect = isLocalFlagEnabled && internalUserDecider.isInternalUser + + return isLocalFlagInEffect || isFeatureFlagEnabled } var isAvailableInPublicRelease: Bool { diff --git a/DuckDuckGo/NewTabPageSectionsDebugView.swift b/DuckDuckGo/NewTabPageSectionsDebugView.swift index fc0f5e634a..1f3a05e198 100644 --- a/DuckDuckGo/NewTabPageSectionsDebugView.swift +++ b/DuckDuckGo/NewTabPageSectionsDebugView.swift @@ -94,7 +94,7 @@ struct NewTabPageSectionsDebugView: View { HStack { VStack { - Text(verbatim: "Feature flag enabled") + Text(verbatim: "Remote feature flag enabled") } Spacer() if newTabPageDebugging.isFeatureFlagEnabled { diff --git a/DuckDuckGo/NewTabPageSettingsSectionItemView.swift b/DuckDuckGo/NewTabPageSettingsSectionItemView.swift index 1ff000b79f..793bb18a11 100644 --- a/DuckDuckGo/NewTabPageSettingsSectionItemView.swift +++ b/DuckDuckGo/NewTabPageSettingsSectionItemView.swift @@ -47,9 +47,9 @@ private extension View { @ViewBuilder func applyListRowInsets() -> some View { if #available(iOS 16, *) { - self + listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 8)) } else { - listRowInsets(EdgeInsets(top: 0, leading: -24, bottom: 0, trailing: 8)) + listRowInsets(EdgeInsets(top: 0, leading: -24, bottom: 0, trailing: 16)) } } } diff --git a/DuckDuckGo/NewTabPageSettingsView.swift b/DuckDuckGo/NewTabPageSettingsView.swift index cf3b3cec2e..c803582193 100644 --- a/DuckDuckGo/NewTabPageSettingsView.swift +++ b/DuckDuckGo/NewTabPageSettingsView.swift @@ -51,14 +51,13 @@ struct NewTabPageSettingsView: View { if sectionsSettingsModel.enabledItems.contains(.shortcuts) { GeometryReader { geometry in ScrollView { - VStack { + VStack(spacing: 0) { sectionsList(withFrameUpdates: true, geometry: geometry) .withoutScroll() .frame(height: listHeight) - EditableShortcutsView(model: shortcutsSettingsModel) + EditableShortcutsView(model: shortcutsSettingsModel, geometry: geometry) .padding(.horizontal, Metrics.horizontalPadding) - .coordinateSpace(name: Constant.scrollCoordinateSpaceName) } } } @@ -105,11 +104,11 @@ struct NewTabPageSettingsView: View { ForEach(sectionsSettingsModel.itemsSettings, id: \.item) { setting in switch setting.item { case .favorites: - NewTabPageSettingsSectionItemView(title: "Favorites", + NewTabPageSettingsSectionItemView(title: UserText.newTabPageSettingsSectionNameFavorites, iconResource: .favorite24, isEnabled: setting.isEnabled) case .shortcuts: - NewTabPageSettingsSectionItemView(title: "Shortcuts", + NewTabPageSettingsSectionItemView(title: UserText.newTabPageSettingsSectionNameShortcuts, iconResource: .shortcut24, isEnabled: setting.isEnabled) } @@ -119,16 +118,8 @@ struct NewTabPageSettingsView: View { } } -private struct Constant { - static let scrollCoordinateSpaceName = "Scroll" -} - -private extension CoordinateSpace { - static let scroll = CoordinateSpace.named(Constant.scrollCoordinateSpaceName) -} - private struct Metrics { - static let horizontalPadding = 16.0 + static let horizontalPadding = 24.0 static let initialListHeight = 5000.0 } diff --git a/DuckDuckGo/NewTabPageShortcut.swift b/DuckDuckGo/NewTabPageShortcut.swift index 2f837bec63..16fea66723 100644 --- a/DuckDuckGo/NewTabPageShortcut.swift +++ b/DuckDuckGo/NewTabPageShortcut.swift @@ -23,6 +23,10 @@ enum NewTabPageShortcut: CaseIterable, Equatable, Identifiable, Codable { var id: String { storageIdentifier } case bookmarks, aiChat, passwords, downloads, settings + + static var enabledByDefault: [NewTabPageShortcut] { + NewTabPageShortcut.allCases.filter { $0 != .aiChat } + } } extension NewTabPageShortcut { diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift index c769e31b44..7560398968 100644 --- a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift +++ b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift @@ -25,6 +25,6 @@ extension NewTabPageSettingsPersistentStorage { convenience init() { self.init(keyPath: \.newTabPageShortcutsSettings, defaultOrder: NewTabPageShortcut.allCases, - defaultEnabledItems: NewTabPageShortcut.allCases) + defaultEnabledItems: NewTabPageShortcut.enabledByDefault) } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index ccf007ff5c..ecc09bbba7 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -31,6 +31,8 @@ struct NewTabPageView= currentStackHeight + buttonVSpaceRequired + + // If there's no room, show the button inside the stack view, after sections + return !buttonHasRoomInViewport + }) + .onPreferenceChange(CustomizeButtonPrefKey.self, perform: { value in + customizeButtonShowedInline = isAnySectionEnabled ? value : false + }) + } + .withScrollKeyboardDismiss() + .safeAreaInset(edge: .bottom, alignment: .trailing) { + if !customizeButtonShowedInline { + customizeButtonView + .frame(maxWidth: .infinity) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .padding([.trailing, .bottom], Metrics.largePadding) + } + } + } + } + + @ViewBuilder + private var emptyStateView: some View { + ZStack { + NewTabPageDaxLogoView() + + VStack(spacing: Metrics.sectionSpacing) { + introMessageView + .padding(.top, Metrics.nonGridSectionTopPadding) + + messagesSectionView + .padding(.top, Metrics.nonGridSectionTopPadding) + .frame(maxHeight: .infinity, alignment: .top) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .safeAreaInset(edge: .bottom, alignment: .trailing) { + customizeButtonView + .frame(maxWidth: .infinity) + } + } + .padding(Metrics.largePadding) + } + private var messagesSectionView: some View { ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in HomeMessageView(viewModel: messageModel) - .frame(maxWidth: horizontalSizeClass == .regular ? Constant.messageMaximumWidthPad : Constant.messageMaximumWidth) - .padding(16) + .frame(maxWidth: horizontalSizeClass == .regular ? Metrics.messageMaximumWidthPad : Metrics.messageMaximumWidth) + .transition(.scale.combined(with: .opacity)) } } - private var favoritesSectionView: some View { + private func favoritesSectionView(proxy: GeometryProxy) -> some View { Group { if favoritesModel.isEmpty { - FavoritesEmptyStateView(model: favoritesModel) + FavoritesEmptyStateView(model: favoritesModel, geometry: proxy) + .padding(.top, Metrics.nonGridSectionTopPadding) } else { - FavoritesView(model: favoritesModel) + FavoritesView(model: favoritesModel, geometry: proxy) } } - .sectionPadding() } @ViewBuilder - private var shortcutsSectionView: some View { + private func shortcutsSectionView(proxy: GeometryProxy) -> some View { if isShortcutsSectionVisible { - ShortcutsView(model: shortcutsModel, shortcuts: shortcutsSettingsModel.enabledItems) - .sectionPadding() + ShortcutsView(model: shortcutsModel, shortcuts: shortcutsSettingsModel.enabledItems, proxy: proxy) + .transition(.scale.combined(with: .opacity)) } } @@ -85,8 +206,7 @@ struct NewTabPageView some View { - self.padding(Constant.sectionPadding) + @ViewBuilder + func withScrollKeyboardDismiss() -> some View { + if #available(iOS 16, *) { + scrollDismissesKeyboard(.immediately) + } else { + self } } +} -private struct Constant { - static let sectionPadding = EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) +private struct Metrics { + static let regularPadding = 16.0 + static let largePadding = 24.0 + static let sectionSpacing = 32.0 + static let nonGridSectionTopPadding = -8.0 + + static let customizeButtonHeight: CGFloat = 40 static let messageMaximumWidth: CGFloat = 380 static let messageMaximumWidthPad: CGFloat = 455 } +private struct CustomizeButtonPrefKey: PreferenceKey { + static var defaultValue: Bool = true + + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() + } +} + // MARK: - Preview #Preview("Regular") { @@ -222,6 +298,21 @@ private struct Constant { ) } +#Preview("Empty state") { + NewTabPageView( + newTabPageModel: NewTabPageModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesModel: FavoritesPreviewModel(), + shortcutsModel: ShortcutsModel(), + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), + sectionsSettingsModel: NewTabPageSectionsSettingsModel(storage: .emptyStorage()) + ) +} + private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration { private(set) var homeMessages: [HomeMessage] @@ -241,3 +332,9 @@ private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration homeMessages = homeMessages.dropLast() } } + +private extension NewTabPageSectionsSettingsStorage { + static func emptyStorage() -> Self { + Self.init(keyPath: \.newTabPageSectionsSettings, defaultOrder: [], defaultEnabledItems: []) + } +} diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index bb11fcabfc..7e154ff281 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -51,7 +51,8 @@ final class NewTabPageViewController: UIHostingController: View { + typealias ContentBuilder = (Data) -> Content + typealias PreviewBuilder = (Data) -> Preview + private let data: [Data] private let isReorderingEnabled: Bool private let id: KeyPath - private let content: (Data) -> Content - private let preview: ((Data) -> Preview)? + private let content: ContentBuilder + private let preview: PreviewBuilder? private let onMove: (_ from: IndexSet, _ to: Int) -> Void @State private var movedItem: Data? - @State private var isItemLocationChanged: Bool = false init(_ data: [Data], id: KeyPath, isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder content: @escaping ContentBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { self.data = data self.id = id @@ -54,7 +56,7 @@ struct ReorderableForEach, isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder content: @escaping ContentBuilder, @ViewBuilder preview: @escaping (Data) -> Preview, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { self.data = data @@ -95,19 +97,17 @@ struct ReorderableForEach: DropDelegate { +private struct ReorderDropDelegate: DropDelegate { let data: [Data] let item: Data let onMove: (_ from: IndexSet, _ to: Int) -> Void @Binding var movedItem: Data? - @Binding var isItemLocationChanged: Bool func dropEntered(info: DropInfo) { guard item != movedItem, @@ -116,8 +116,6 @@ private struct ReorderDropDelegate: DropDelegate { let to = data.firstIndex(of: item) else { return } - isItemLocationChanged = true - if data[to] != current { let fromIndices = IndexSet(integer: from) let toIndex = to > from ? to + 1 : to @@ -130,16 +128,15 @@ private struct ReorderDropDelegate: DropDelegate { } func performDrop(info: DropInfo) -> Bool { - isItemLocationChanged = false movedItem = nil - return true + return info.hasItemsConforming(to: [item.dropType]) } } extension ReorderableForEach where Data: Identifiable, ID == Data.ID { init(_ data: [Data], isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder content: @escaping ContentBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { self.data = data self.id = \Data.id @@ -151,8 +148,8 @@ extension ReorderableForEach where Data: Identifiable, ID == Data.ID { init(_ data: [Data], isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, - @ViewBuilder preview: @escaping (Data) -> Preview, + @ViewBuilder content: @escaping ContentBuilder, + @ViewBuilder preview: @escaping PreviewBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { self.data = data self.id = \Data.id diff --git a/DuckDuckGo/ShortcutAccessoryView.swift b/DuckDuckGo/ShortcutAccessoryView.swift index a6756901ec..3a269ba930 100644 --- a/DuckDuckGo/ShortcutAccessoryView.swift +++ b/DuckDuckGo/ShortcutAccessoryView.swift @@ -18,28 +18,40 @@ // import SwiftUI +import DuckUI struct ShortcutAccessoryView: View { + @Environment(\.colorScheme) private var colorScheme + let accessoryType: ShortcutAccessoryType - let expectedSize: CGSize var body: some View { Circle() - .foregroundStyle(accessoryType.backgroundColor) + .foregroundStyle(bgColorForAccessoryType(accessoryType)) .overlay { Image(accessoryType.iconResource) + .resizable() .foregroundColor(accessoryType.foregroundColor) .aspectRatio(contentMode: .fit) + .scaleEffect(x: Constant.imageScaleRatio, y: Constant.imageScaleRatio) } .shadow(color: .shade(0.15), radius: 1, y: 1) - .frame(width: expectedSize.width, height: expectedSize.height) } -} -extension ShortcutAccessoryView { - init(accessoryType: ShortcutAccessoryType) { - self.init(accessoryType: accessoryType, expectedSize: CGSize(width: 24, height: 24)) + func bgColorForAccessoryType(_ accessoryType: ShortcutAccessoryType) -> Color { + switch accessoryType { + case .selected: + return Color(designSystemColor: .accent) + case .add: + // One-off exception for this particular case. + // See https://app.asana.com/0/72649045549333/1207988345460434/f + return colorScheme == .dark ? .gray85 : Color(designSystemColor: .surface) + } + } + + private enum Constant { + static let imageScaleRatio: CGFloat = 2.0/3.0 } } @@ -58,15 +70,6 @@ private extension ShortcutAccessoryType { } } - var backgroundColor: Color { - switch self { - case .selected: - Color(designSystemColor: .accent) - case .add: - Color(designSystemColor: .surface) - } - } - var foregroundColor: Color { switch self { case .selected: diff --git a/DuckDuckGo/ShortcutItemView.swift b/DuckDuckGo/ShortcutItemView.swift index deb085d0b0..ad551be8fd 100644 --- a/DuckDuckGo/ShortcutItemView.swift +++ b/DuckDuckGo/ShortcutItemView.swift @@ -29,8 +29,13 @@ struct ShortcutItemView: View { ShortcutIconView(shortcut: shortcut) .overlay(alignment: .topTrailing) { if let accessoryType { - ShortcutAccessoryView(accessoryType: accessoryType) - .alignedForOverlay(edgeSize: Constant.accessorySize) + Group { + let offset = Constant.accessorySize/4.0 + ShortcutAccessoryView(accessoryType: accessoryType) + .frame(width: Constant.accessorySize, height: Constant.accessorySize) + .offset(x: offset, y: -offset) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) } } @@ -52,17 +57,16 @@ struct ShortcutIconView: View { let shortcut: NewTabPageShortcut var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(Color(designSystemColor: .surface)) - .shadow(color: .shade(0.12), radius: 0.5, y: 1) - .aspectRatio(1, contentMode: .fit) - .frame(width: NewTabPageGrid.Item.edgeSize) - Image(shortcut.imageResource) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: NewTabPageGrid.Item.edgeSize * 0.5) - } + RoundedRectangle(cornerRadius: 8) + .fill(Color(designSystemColor: .surface)) + .shadow(color: .shade(0.12), radius: 0.5, y: 1) + .aspectRatio(1, contentMode: .fit) + .overlay { + Image(shortcut.imageResource) + .resizable() + .aspectRatio(contentMode: .fit) + .scaleEffect(x: 0.5, y: 0.5) + } } } @@ -98,30 +102,16 @@ private extension NewTabPageShortcut { } } -private extension ShortcutAccessoryView { - @ViewBuilder func alignedForOverlay(edgeSize: CGFloat) -> some View { - let offset = CGSize(width: edgeSize/4.0, height: -edgeSize/4.0) -// let size = CGSize(width: edgeSize, height: edgeSize) - - if #available(iOS 16, *) { - frame(width: edgeSize) - .offset(offset) - } else { - frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .offset(offset) - } - } -} - #Preview { ScrollView { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 86))], content: { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 68), spacing: 8, alignment: .top)], content: { let accessoryTypes: [ShortcutAccessoryType?] = [.none, .add, .selected] ForEach(accessoryTypes, id: \.?.hashValue) { type in Section { ForEach(NewTabPageShortcut.allCases) { shortcut in ShortcutItemView(shortcut: shortcut, accessoryType: type) + .frame(width: 64) } } footer: { diff --git a/DuckDuckGo/ShortcutsView.swift b/DuckDuckGo/ShortcutsView.swift index 9ff0c6a2b2..230d2a5393 100644 --- a/DuckDuckGo/ShortcutsView.swift +++ b/DuckDuckGo/ShortcutsView.swift @@ -23,9 +23,10 @@ import UniformTypeIdentifiers struct ShortcutsView: View { private(set) var model: ShortcutsModel let shortcuts: [NewTabPageShortcut] + let proxy: GeometryProxy? var body: some View { - NewTabPageGridView { _ in + NewTabPageGridView(geometry: proxy) { _ in ForEach(shortcuts) { shortcut in Button { model.openShortcut(shortcut) @@ -39,7 +40,7 @@ struct ShortcutsView: View { #Preview { ScrollView { - ShortcutsView(model: ShortcutsModel(), shortcuts: NewTabPageShortcut.allCases) + ShortcutsView(model: ShortcutsModel(), shortcuts: NewTabPageShortcut.allCases, proxy: nil) } .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/ToggleExpandButtonView.swift b/DuckDuckGo/ToggleExpandButtonStyle.swift similarity index 65% rename from DuckDuckGo/ToggleExpandButtonView.swift rename to DuckDuckGo/ToggleExpandButtonStyle.swift index eb4a9ac695..b36c9d2d33 100644 --- a/DuckDuckGo/ToggleExpandButtonView.swift +++ b/DuckDuckGo/ToggleExpandButtonStyle.swift @@ -1,5 +1,5 @@ // -// ToggleExpandButtonView.swift +// ToggleExpandButtonStyle.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -30,26 +30,31 @@ struct ToggleExpandButtonStyle: ButtonStyle { VStack { ExpandButtonDivider() } - ZStack { - Circle() - .stroke(Color(designSystemColor: .lines), lineWidth: 1) - .frame(width: 32) - .if(configuration.isPressed, transform: { - $0.background(Circle() - .fill(isDark ? Color.tint(0.12) : Color.shade(0.06))) - }) - .background( - Circle() - .fill(Color(designSystemColor: .background)) - ) - configuration.label - .foregroundColor(isDark ? .tint(0.6) : .shade(0.6)) - .frame(width: 16, height: 16) - } + + Circle() + .stroke(Color(designSystemColor: .lines), lineWidth: 1) + .frame(width: 32, height: 32) + .if(configuration.isPressed) { + $0.background(isDark ? Color.tint(0.12) : Color.shade(0.06)) + .clipShape(Circle()) + } + .background( + Circle() + .fill(Color(designSystemColor: .background)) + ) + .overlay { + configuration.label + .foregroundColor(isDark ? .tint(0.6) : .shade(0.6)) + .frame(width: 16, height: 16) + + } + VStack { ExpandButtonDivider() } } + .padding(.vertical, 0.5) // Adjust padding for drawing group, otherwise the circle stroke is clipped + .drawingGroup() } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 8410fd0612..0f6a604a29 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1289,6 +1289,8 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: - New Tab Page + public static let newTabPageFavoritesSectionHeaderTitle = NSLocalizedString("new.tab.page.favorites.setion.header.title", value: "Favorites", comment: "Header title of Favorites section") + // MARK: Shortcuts public static let newTabPageShortcutBookmarks = NSLocalizedString("new.tab.page.shortcut.bookmarks", value: "Bookmarks", comment: "Shortcut title leading to Bookmarks") public static let newTabPageShortcutAIChat = NSLocalizedString("new.tab.page.shortcut.ai.chat", value: "AI Chat", comment: "Shortcut title leading to AI Chat") @@ -1302,10 +1304,12 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: Settings - public static let newTabPageSettingsTitle = NSLocalizedString("new.tab.page.settings.title", value: "Customize New Tab", comment: "Title of New Tab Page preferences page.") + public static let newTabPageSettingsTitle = NSLocalizedString("new.tab.page.settings.title", value: "Customize New Tab Page", comment: "Title of New Tab Page preferences page.") public static let newTabPageSettingsSectionsHeaderTitle = NSLocalizedString("new.tab.page.settings.sections.header.title", value: "SECTIONS", comment: "Header title of the group allowing for setting up new tab page sections") public static let newTabPageSettingsSectionsDescription = NSLocalizedString("new.tab.page.settings.sections.description", value: "Show, hide, and reorder sections on the new tab page", comment: "Footer of the group allowing for setting up new tab page sections") public static let newTabPageSettingsShortcutsHeaderTitle = NSLocalizedString("new.tab.page.settings.shortcuts.header.title", value: "SHORTCUTS", comment: "Header title of the shortcuts in New Tab Page preferences.") + public static let newTabPageSettingsSectionNameFavorites = NSLocalizedString("new.tab.page.settings.section.name.favorites", value: "Favorites", comment: "Name of favorites section setting") + public static let newTabPageSettingsSectionNameShortcuts = NSLocalizedString("new.tab.page.settings.section.name.shortcuts", value: "Shortcuts", comment: "Name of shortcuts section setting") // MARK: Intro message diff --git a/DuckDuckGo/ViewExtension.swift b/DuckDuckGo/ViewExtension.swift index eefb1e6b66..ca1afc56cc 100644 --- a/DuckDuckGo/ViewExtension.swift +++ b/DuckDuckGo/ViewExtension.swift @@ -35,7 +35,8 @@ extension View { } extension View { - /// Disables scroll if allowed by system version + /// Disables scroll if available for current system version + @available(iOS, deprecated: 16.0, renamed: "scrollDisabled") @ViewBuilder func withoutScroll(_ isScrollDisabled: Bool = true) -> some View { if #available(iOS 16, *) { diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index bbea086b15..35bb6aa4a9 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Премахване"; -/* No comment provided by engineer. */ -"Favorites" = "Любими"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Всички любими, запазени на устройството"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Само любими на мобилни устройства"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Блокиране на реклами и изскачащи прозорци"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Добре дошли в\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Настройки на VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Любими"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Персонализирайте раздела Любими и функциите за достъп. Пренаредете елементите или скрийте някои от тях за по голяма яснота."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Показване, скриване и пренареждане на секции на страницата с нов раздел"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Страницата с нов раздел е... Нова!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Любими"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Бързи Връзки"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Показване, скриване и пренареждане на секции на страницата с нов раздел"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "СЕКЦИИ"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Отваряне в друго приложение?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Търсене или въвеждане на адрес"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index df08ed3417..72f786bc8d 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Odstranit"; -/* No comment provided by engineer. */ -"Favorites" = "Oblíbené"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Všechny oblíbené položky na zařízení"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Jenom oblíbené mobilní položky"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokování reklam a vyskakovacích oken"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Vítejte na\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Nastavení sítě VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Oblíbené"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Přizpůsob si oblíbené záložky a časté funkce. Přeskládej je nebo je skryj, ať je všechno přehledné."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Zobrazení, skrytí a změna pořadí oddílů na stránce nové karty"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Stránka nové karty je... úplně nová!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Oblíbené"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Zkratky"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Zobrazení, skrytí a změna pořadí oddílů na stránce nové karty"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ODDÍLY"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "ZKRATKY"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Přizpůsobení nové karty"; +"new.tab.page.settings.title" = "Přizpůsobení karty s novou stránkou"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otevřít v jiné aplikaci?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Vyhledejte nebo zadejte adresu"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index 89a6c47576..cf77933d37 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Fjern"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritter"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle favoritter på enheden"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Kun favoritter på mobil"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokering af annoncer og pop op-vinduer"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Velkommen til\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-indstillinger"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritter"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Tilpas dine favoritter og go-to-funktioner. Flyt rundt på ting, eller skjul dem for at holde det enkelt."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Vis, skjul og omarranger sektioner på den nye faneside"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Din nye faneside er... Ny!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritter"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Genveje"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Vis, skjul og omarranger sektioner på den nye faneside"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEKTIONER"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "GENVEJE"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Tilpas ny fane"; +"new.tab.page.settings.title" = "Tilpas siden Ny fane"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI-chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Åbn i en anden app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Søg eller indtast adresse"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 08eaf46ebc..6459a2bbce 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Entfernen"; -/* No comment provided by engineer. */ -"Favorites" = "Favoriten"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle Gerätefavoriten"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Nur mobile Favoriten"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blockieren von Werbungen und Pop-ups"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Willkommen bei\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-Einstellungen"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoriten"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Passe deine Favoriten und häufig genutzten Funktionen an. Ordne die Dinge neu an oder blende sie aus, um die Übersichtlichkeit zu wahren."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Abschnitte auf der neuen Tab-Seite anzeigen, ausblenden und neu anordnen"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Deine „Neuer Tab“-Seite ist ... neu!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoriten"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Shortcuts"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Abschnitte auf der neuen Tab-Seite anzeigen, ausblenden und neu anordnen"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ABSCHNITTE"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "In einer anderen App öffnen?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Adresse suchen oder eingeben"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index aa144e9126..4d4724f848 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Αφαίρεση"; -/* No comment provided by engineer. */ -"Favorites" = "Αγαπημένα"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Αγαπημένα όλων των συσκευών"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Αγαπημένα μόνο για κινητά"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Αποκλεισμός διαφημίσεων και αναδυόμενων παραθύρων"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Καλώς ορίσατε στο\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Ρυθμίσεις VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Αγαπημένα"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Προσαρμόστε τα Αγαπημένα και τις λειτουργίες μετάβασης. Αναδιατάξτε στοιχεία ή αποκρύψτε τα για να τη διατηρείτε καθαρή."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Εμφάνιση, απόκρυψη και αναδιάταξη ενοτήτων στη σελίδα της νέας καρτέλας"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Η σελίδα σας για τη Νέα καρτέλα είναι... Νέα!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Αγαπημένα"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Συντομευσεις"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Εμφάνιση, απόκρυψη και αναδιάταξη ενοτήτων στη σελίδα της νέας καρτέλας"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ΕΝΟΤΗΤΕΣ"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "ΣΥΝΤΟΜΕΥΣΕΙΣ"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Προσαρμογή νέας καρτέλας"; +"new.tab.page.settings.title" = "Προσαρμογή σελίδας νέας καρτέλας"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Συνομιλία με ΤΝ"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Άνοιγμα σε άλλη εφαρμογή;"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Αναζήτηση ή εισαγωγή διεύθυνσης"; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index ec91ba69c6..1bdd754039 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1775,12 +1775,21 @@ https://duckduckgo.com/mac"; /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Settings"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favorites"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Customize your Favorites and go-to features. Reorder things or hide them to keep it clean."; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Your New Tab Page is... New!"; +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favorites"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Shortcuts"; + /* Footer of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.description" = "Show, hide, and reorder sections on the new tab page"; @@ -1791,7 +1800,7 @@ https://duckduckgo.com/mac"; "new.tab.page.settings.shortcuts.header.title" = "SHORTCUTS"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Customize New Tab"; +"new.tab.page.settings.title" = "Customize New Tab Page"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 302fa4a7b2..0751644415 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Eliminar"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritos"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Todos los favoritos del dispositivo"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Solo favoritos móviles"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Bloquear anuncios y mensajes emergentes"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "¡Bienvenido a\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Configuración de VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritos"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personaliza tus favoritos y funciones de acceso. Reordena los elementos o escóndelos para mantener el orden."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Mostrar, ocultar y reordenar las secciones de la página nueva pestaña"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Tu nueva pestaña es... ¡Novedad!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritos"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Accesos directos"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Mostrar, ocultar y reordenar las secciones de la página nueva pestaña"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECCIONES"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Accesos directos"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personaliza la nueva pestaña"; +"new.tab.page.settings.title" = "Personalizar página Nueva pestaña"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat de IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "¿Abrir en otra aplicación?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Buscar o introducir dirección"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index 653a267f8c..8a72451e11 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Eemaldage"; -/* No comment provided by engineer. */ -"Favorites" = "Lemmikud"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Kõik seadme lemmikud"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Ainult mobiililemmikud"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklaami ja hüpikakende blokeerimine"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Tere tulemast\nDuckDuckGo kasutajaks!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-i seaded"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Lemmikud"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Kohanda oma lemmikuid ja enim kasutatavaid funktsioone. Järjesta valikud ümber või peida need, et kõik oleks lihtsasti leitav."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Jaotiste kuvamine, peitmine ja järjestamine uuel vahelehel"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Sinu uue vahelehe leht on... Uus!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Lemmikud"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Otseteed"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Jaotiste kuvamine, peitmine ja järjestamine uuel vahelehel"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "JAOTISED"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "OTSETEED"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Uue vahekaardi kohandamine"; +"new.tab.page.settings.title" = "Uue vahekaardilehe kohandamine"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI vestlus"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Avada teises rakenduses?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Otsi või sisesta aadress"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 20845d22c2..85ee101acb 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Poista"; -/* No comment provided by engineer. */ -"Favorites" = "Suosikit"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Kaikki laitteen suosikit"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Vain mobiilisuosikit"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Mainosten ja ponnahdusikkunoiden esto"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Tervetuloa\nDuckDuckGo-sovellukseen!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-asetukset"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Suosikit"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Mukauta suosikkejasi ja muita usein käyttämiäsi ominaisuuksia. Luo uusi järjestys tai piilota kohteita pitääksesi selkeän ilmeen."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Näytä, piilota ja järjestä uuden välilehden osiot uudelleen"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Uusi välilehti -sivusi on... uusi!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Suosikit"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Pikavalinnat"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Näytä, piilota ja järjestä uuden välilehden osiot uudelleen"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "OSIOT"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "PIKAVALINNAT"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Mukauta uusi välilehti"; +"new.tab.page.settings.title" = "Mukauta uuden välilehden sivua"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Tekoäly-chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Avataanko toisessa sovelluksessa?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Hae tai anna osoite"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index 9e355d73ce..6c9382eb6a 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Supprimer"; -/* No comment provided by engineer. */ -"Favorites" = "Favoris"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Tous les favoris de l'appareil"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Favoris sur mobile uniquement"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Bloquer les publicités et les fenêtres contextuelles"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Bienvenue sur\nDuckDuckGo !"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Paramètres VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoris"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personnalisez vos favoris et vos fonctionnalités préférées. Réorganisez les différents éléments ou masquez-les pour que tout reste en ordre."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Afficher, masquer et réorganiser les sections sur la nouvelle page d'onglet"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Votre nouvelle page d'onglet est… toute nouvelle !"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoris"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Raccourcis"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Afficher, masquer et réorganiser les sections sur la nouvelle page d'onglet"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECTIONS"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Raccourcis"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personnaliser le nouvel onglet"; +"new.tab.page.settings.title" = "Personnaliser la nouvelle page d'onglet"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Ouvrir dans une autre application ?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Rechercher ou saisir une adresse"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 3d89765b7e..8e72639a3d 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Ukloni"; -/* No comment provided by engineer. */ -"Favorites" = "Omiljeno"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Svi favoriti na uređaju"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Samo favoriti na mobilnom uređaju"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokiranje oglasa i skočnih prozora"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Dobro došao/la u\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN postavke"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Omiljeno"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Prilagodi svoje favorite i najdraže značajke. Preuredi stvari ili ih sakrij kako bi sve bilo jasno."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Prikaži, sakrij i promijeni redoslijed odjeljaka na stranici nove kartice"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Tvoja stranica nove kartice je... nova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Omiljeno"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Prečaci"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Prikaži, sakrij i promijeni redoslijed odjeljaka na stranici nove kartice"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEKCIJE"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "PREČACI"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Prilagodi novu karticu"; +"new.tab.page.settings.title" = "Prilagodi stranicu Nova kartica"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otvori u drugoj aplikaciji?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Pretraži ili unesi adresu"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 941efc0b26..ea6c868cba 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Eltávolítás"; -/* No comment provided by engineer. */ -"Favorites" = "Kedvencek"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Eszközön lévő minden kedvenc"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Csak mobilon lévő kedvencek"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Hirdetések és felugró ablakok letiltása"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Üdvözlünk a\nDuckDuckGo-ban!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-beállítások"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Kedvencek"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Testre szabhatja a kedvenceket és a leggyakrabban használt funkciókat. Az áttekinthetőség érdekében átrendezheti az elemeket, vagy elrejtheti őket."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Részek megjelenítése, elrejtése és átrendezése az új lap oldalon"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Az új lap oldal… Új!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Kedvencek"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Gyorsparancsok"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Részek megjelenítése, elrejtése és átrendezése az új lap oldalon"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "RÉSZEK"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Megnyitás másik alkalmazásban?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Keresés vagy cím megadása"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index f35bd757bc..4e80e35737 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Rimuovi"; -/* No comment provided by engineer. */ -"Favorites" = "Preferiti"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Tutti i preferiti sul dispositivo"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Solo preferiti sul dispositivo mobile"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blocco di annunci e popup"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "DuckDuckGo\nti dà il benvenuto!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Impostazioni VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Preferiti"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personalizza i tuoi Preferiti e le funzionalità principali. Riorganizza gli elementi o nascondili per mantenere tutto in ordine."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Mostra, nascondi e riordina le sezioni nella nuova scheda"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "La tua pagina Nuova scheda è... Nuova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Preferiti"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Scorciatoie"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Mostra, nascondi e riordina le sezioni nella nuova scheda"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEZIONI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Scorciatoie"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personalizza la pagina Nuova scheda"; +"new.tab.page.settings.title" = "Personalizza pagina Nuova scheda"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Aprire in un'altra app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Cerca o digita l'indirizzo"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index 950fa90f78..193dfdf75a 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Pašalinti"; -/* No comment provided by engineer. */ -"Favorites" = "Mėgstami"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Visi įrenginio mėgstamiausi"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Tik mobiliojo mėgstamiausi"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklamų ir iššokančiųjų langų blokavimas"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Sveiki atvykę į\n„DuckDuckGo“!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN nustatymai"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Mėgstami"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Tinkinkite savo mėgstamiausius ir pagrindines funkcijas. Pertvarkykite elementus arba paslėpkite juos, kad užtikrintumėte tvarką."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Rodyti, slėpti ir pertvarkyti skiltis naujame skirtuko puslapyje"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Jūsų naujo skirtuko puslapis yra... Atnaujintas!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Mėgstami"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Šaukiniai"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Rodyti, slėpti ir pertvarkyti skiltis naujame skirtuko puslapyje"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SKILTYS"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "ŠAUKINIAI"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Tinkinti naują skirtuką"; +"new.tab.page.settings.title" = "Tinkinti naujo skirtuko puslapį"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "DI pokalbis"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Atidaryti kitoje programoje?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Ieškoti arba įvesti adresą"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 32f3209f50..9fde6bb665 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Noņemt"; -/* No comment provided by engineer. */ -"Favorites" = "Izlase"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Visu ierīču izlase"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Tikai mobilā izlase"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklāmu un uznirstošo logu bloķēšana"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Laipni lūdzam\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN iestatījumi"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Izlase"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Pielāgo savu izlasi un bieži izmantotās funkcijas. Pārkārto elementus vai paslēp tos, lai lieki neaizņemtu vietu."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Rādīt, paslēpt un pārkārtot sadaļas jaunās cilnes lapā"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Tava jaunas cilnes lapa ir... atjaunota!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Izlase"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Saīsnes"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Rādīt, paslēpt un pārkārtot sadaļas jaunās cilnes lapā"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SADAĻAS"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SAĪSNES"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Jaunas cilnes pielāgošana"; +"new.tab.page.settings.title" = "Jaunas cilnes lapas pielāgošana"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "MI čats"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Vai atvērt citā lietotnē?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Meklē vai ievadi adresi"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index f525e7a8d6..320d6c30a4 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Fjern"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritter"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle favoritter på enheten"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Kun favoritter på mobil"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokkering av reklame og popups"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Velkommen til\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-innstillinger"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritter"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Tilpass favorittene dine og de funksjonene du bruker mest. Endre på rekkefølgen av ting eller skjul dem for å holde orden."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Vis, skjul og endre rekkefølgen av delene på den nye fanesiden"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Den nye fanesiden din er … ny!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritter"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Snarveier"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Vis, skjul og endre rekkefølgen av delene på den nye fanesiden"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "DELER"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SNARVEIER"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Tilpass ny fane"; +"new.tab.page.settings.title" = "Tilpass ny faneside"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "KI-chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Åpne i en annen app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Søk eller skriv inn adresse"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index 94c0e486c7..c4e0a07bb0 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Verwijderen"; -/* No comment provided by engineer. */ -"Favorites" = "Favorieten"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle favorieten op apparaat"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Alleen mobiele favorieten"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Advertentie- en pop-upblokkering"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Welkom bij\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-instellingen"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favorieten"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Pas je favorieten en go-to-functies aan. Herschik dingen of verberg ze om de tabbladpagina overzichtelijk te maken."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Secties op het nieuwe tabblad weergeven, verbergen en opnieuw ordenen"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Je nieuwe tabbladpagina is... Nieuw!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favorieten"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Sneltoetsen"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Secties op het nieuwe tabblad weergeven, verbergen en opnieuw ordenen"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECTIES"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Openen in een andere app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Zoek of voer een adres in"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 4a86e99373..aa0e54a7ef 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Usuń"; -/* No comment provided by engineer. */ -"Favorites" = "Ulubione"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Ulubione na urządzeniu"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Tylko ulubione z urządzeń mobilnych"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokowanie reklam i wyskakujących okienek"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Witamy\nw DuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Ustawienia VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Ulubione"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Dostosuj ulubione elementy i podręczne funkcje. Zmień kolejność opcji lub ukryj je, aby zachować porządek."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Pokazuj, ukrywaj i zmieniaj kolejność sekcji na stronie nowej karty"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Strona nowej karty ma nowy wygląd!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Ulubione"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Skróty"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Pokazuj, ukrywaj i zmieniaj kolejność sekcji na stronie nowej karty"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEKCJE"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Skróty"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Dostosuj nową kartę"; +"new.tab.page.settings.title" = "Dostosuj stronę nowej karty"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Czat AI"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otworzyć w innej aplikacji?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Wyszukaj lub wprowadź adres"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index 7694c90646..26a07512de 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Remover"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritos"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Todos os favoritos no dispositivo"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Apenas favoritos em dispositivos móveis"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Bloquear anúncios e pop-ups"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Damos-lhe as boas-vindas ao\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Definições da VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritos"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personaliza os teus Favoritos e as tuas funcionalidades de preferência. Reordena as coisas ou esconde-as para manter um aspeto limpo."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Mostrar, ocultar e reordenar secções na página do novo separador"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "A tua página Novo separador é... nova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritos"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Atalhos"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Mostrar, ocultar e reordenar secções na página do novo separador"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECÇÕES"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Atalhos"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personalizar novo separador"; +"new.tab.page.settings.title" = "Personalizar página Novo separador"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat com IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Abrir noutra aplicação?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Pesquisar ou inserir endereço"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index a5ed9bdb4f..a5b21cd810 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Elimină"; -/* No comment provided by engineer. */ -"Favorites" = "Preferințe"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Toate favoritele de pe dispozitiv"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Doar favoritele pentru mobil"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blocarea anunțurilor și ferestrelor pop-up"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Bine ai venit la\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Setări VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Preferințe"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personalizează-ți Favoritele și funcțiile predilecte. Reordonează lucrurile sau ascunde-le pentru a menține ordinea."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Afișează, ascunde și reordonează secțiunile din pagina noii file"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Pagina Filă nouă este... nouă!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Preferințe"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Scurtături"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Afișează, ascunde și reordonează secțiunile din pagina noii file"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECȚIUNI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SCURTĂTURI"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personalizează noua filă"; +"new.tab.page.settings.title" = "Personalizează pagina Filă nouă"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Deschizi în altă aplicație?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Caută sau introdu adresa"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 9a18920a04..24e34238ce 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Удалить"; -/* No comment provided by engineer. */ -"Favorites" = "Избранное"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Избранное со всех устройств"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Только избранное с телефона"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Блокировка рекламы и всплывающих окон"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Добро пожаловать в\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Настройки VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Избранное"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "К вашим услугам настройки «Избранного» и важнейших функций. Для поддержания порядка отдельные элементы можно переупорядочить или скрыть."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Показать, скрыть или изменить порядок разделов на странице новой вкладки"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Ваша страница новой вкладки... В новом виде!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Избранное"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Ярлыки"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Показать, скрыть или изменить порядок разделов на странице новой вкладки"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "РАЗДЕЛЫ"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Ярлыки"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Настройки страницы новой вкладки"; +"new.tab.page.settings.title" = "Страница новой вкладки"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Чат с ИИ"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Открыть в другом приложении?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Введите поисковый запрос или адрес сайта"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 8dfdde334b..ab8ce098ee 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Odstrániť"; -/* No comment provided by engineer. */ -"Favorites" = "Obľúbené položky"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Obľúbené položky zo všetkých zariadení"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Iba obľúbené mobilné položky"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokovanie reklám a automaticky otváraných okien"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Vitajte v\nprehliadači DuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Nastavenia siete VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Obľúbené položky"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Prispôsobenie obľúbených položiek a funkcií. Pre prehľadnosť zmeňte usporiadanie vecí alebo ich skryte."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Zobrazenie, skrytie a zmena poradia častí na novej stránke karty"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Vaša stránka Nová karta je... Nová!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Obľúbené položky"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Skratky"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Zobrazenie, skrytie a zmena poradia častí na novej stránke karty"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ČASTI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SKRATKY"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Prispôsobiť novú kartu"; +"new.tab.page.settings.title" = "Prispôsobiť stránku novej karty"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otvoriť v inej aplikácii?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Vyhľadajte alebo zadajte adresu"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 8e47d3a97f..1606d05a14 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Odstrani"; -/* No comment provided by engineer. */ -"Favorites" = "Priljubljeni"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Priljubljene iz vseh naprav"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Priljubljene samo iz mobilne naprave"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokiranje oglasov in pojavnih oken"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Dobrodošli v aplikaciji\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Nastavitve VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Priljubljeni"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Prilagodite zavihek Priljubljene in izbrane funkcije. Preuredite stvari ali jih skrijte, da boste imeli vse urejeno."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Prikaz, skrivanje in razvrščanje razdelkov na strani novega zavihka"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Vaša stran za nov zavihek je ... nova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Priljubljeni"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Bližnjice"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Prikaz, skrivanje in razvrščanje razdelkov na strani novega zavihka"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "RAZDELKI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "BLIŽNJICE"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Prilagoditev strani novega zavihka"; +"new.tab.page.settings.title" = "Prilagoditev strani z novim zavihkom"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Klepet z umetno inteligenco"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Odpri v drugi aplikaciji?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Poišči ali vnesi naslov"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index e1bd795f64..d90d21d646 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Ta bort"; -/* No comment provided by engineer. */ -"Favorites" = "Favoriter"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alla enhetsfavoriter"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Endast mobila favoriter"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blockering av annonser och poppuppfönster"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Välkommen till\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-inställningar"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoriter"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Anpassa dina favoriter och funktioner. Ordna om saker och ting eller göm dem för att hålla layouten minimalistisk."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Visa, dölja och ändra ordning på avsnitt på den nya fliksidan"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Din nya fliksida är... ny!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoriter"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Genvägar"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Visa, dölja och ändra ordning på avsnitt på den nya fliksidan"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "AVSNITT"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "GENVÄGAR"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Anpassa ny flik"; +"new.tab.page.settings.title" = "Anpassa sidan Ny flik"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI-chatt"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Öppna i annan app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Sök eller ange adress"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index b6641c68e0..28cfbe8e97 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Kaldır"; -/* No comment provided by engineer. */ -"Favorites" = "Favoriler"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Tüm Cihaz Sık Kullanılanları"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Yalnızca Mobil Favoriler"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklam ve pop-up engelleme"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "DuckDuckGo'ya\nHoş Geldiniz!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Ayarları"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoriler"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Favorilerinizi ve düzenli olarak kullandığınız özellikleri kendinize göre ayarlayın. Temiz tutmak için öğeleri yeniden sıralayın veya gizleyin."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Yeni sekme sayfasında bölümleri göster, gizle ve yeniden sırala"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Yeni Sekme Sayfanız... Yeni!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoriler"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Kısayollar"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Yeni sekme sayfasında bölümleri göster, gizle ve yeniden sırala"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "BÖLÜMLER"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "KISAYOLLAR"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Yeni Sekme Sayfasını Özelleştirin"; +"new.tab.page.settings.title" = "Yeni Sekme Sayfasını Özelleştir"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Yapay Zeka Sohbeti"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Başka Bir Uygulamada Açılsın mı?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Adres ara veya gir"; diff --git a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift index 8e4aff669b..77776002ae 100644 --- a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift +++ b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift @@ -106,6 +106,7 @@ final class NewTabPageFavoritesModelTests: XCTestCase { private func createSUT() -> FavoritesDefaultModel { FavoritesDefaultModel(interactionModel: favoritesListInteracting, + faviconLoader: FavoritesFaviconLoader(), pixelFiring: PixelFiringMock.self, dailyPixelFiring: PixelFiringMock.self) } From 35b7a452d3028428033b1ff29ac878bbe0a858ea Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Mon, 16 Sep 2024 06:29:23 -0500 Subject: [PATCH 02/46] Add privacy config version to broken site reports (#3351) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/AutofillLoginListViewModel.swift | 1 + .../PrivacyDashboard/PrivacyDashboardViewController.swift | 1 + DuckDuckGoTests/BrokenSiteReportingTests.swift | 2 ++ DuckDuckGoTests/MockPrivacyConfiguration.swift | 1 + DuckDuckGoTests/PrivacyConfigurationManagerMock.swift | 1 + 7 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23688459f1..4fc50dc02a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10885,7 +10885,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 195.0.0; + version = 196.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d785772d67..1fc9b47178 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" : "f9134f887b1215779a1050134d09d7e824a8abc0", - "version" : "195.0.0" + "revision" : "ae3dbec01b8b72dc2ea4c510aecbc802862eab63", + "version" : "196.0.0" } }, { diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 0bf99e6fe1..c244f97752 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -362,6 +362,7 @@ final class AutofillLoginListViewModel: ObservableObject { manufacturer: "", upgradedHttps: false, tdsETag: nil, + configVersion: nil, blockedTrackerDomains: nil, installedSurrogates: nil, isGPCEnabled: true, diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift index f16e209939..1c3161a27f 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift @@ -356,6 +356,7 @@ extension PrivacyDashboardViewController { manufacturer: "Apple", upgradedHttps: breakageAdditionalInfo.httpsForced, tdsETag: ContentBlocking.shared.contentBlockingManager.currentMainRules?.etag ?? "", + configVersion: privacyConfigurationManager.privacyConfig.version, blockedTrackerDomains: blockedTrackerDomains, installedSurrogates: privacyInfo.trackerInfo.installedSurrogates.map { $0 }, isGPCEnabled: AppDependencyProvider.shared.appSettings.sendDoNotSell, diff --git a/DuckDuckGoTests/BrokenSiteReportingTests.swift b/DuckDuckGoTests/BrokenSiteReportingTests.swift index a99c8b8f1f..708c5d59d8 100644 --- a/DuckDuckGoTests/BrokenSiteReportingTests.swift +++ b/DuckDuckGoTests/BrokenSiteReportingTests.swift @@ -102,6 +102,7 @@ final class BrokenSiteReportingTests: XCTestCase { manufacturer: test.manufacturer ?? "", upgradedHttps: test.wasUpgraded, tdsETag: test.blocklistVersion, + configVersion: test.remoteConfigVersion, blockedTrackerDomains: test.blockedTrackers, installedSurrogates: test.surrogates, isGPCEnabled: test.gpcEnabled ?? false, @@ -169,6 +170,7 @@ private struct Test: Codable { let providedDescription: String? let blockedTrackers, surrogates: [String] let atb, blocklistVersion: String + let remoteConfigVersion: String? let expectReportURLPrefix: String let expectReportURLParams: [ExpectReportURLParam] let exceptPlatforms: [String] diff --git a/DuckDuckGoTests/MockPrivacyConfiguration.swift b/DuckDuckGoTests/MockPrivacyConfiguration.swift index 1053217167..3066b1c3a6 100644 --- a/DuckDuckGoTests/MockPrivacyConfiguration.swift +++ b/DuckDuckGoTests/MockPrivacyConfiguration.swift @@ -36,6 +36,7 @@ class MockPrivacyConfiguration: PrivacyConfiguration { } var identifier: String = "MockPrivacyConfiguration" + var version: String? = "123456789" var userUnprotectedDomains: [String] = [] var tempUnprotectedDomains: [String] = [] var trackerAllowlist: PrivacyConfigurationData.TrackerAllowlist = .init(entries: [:], diff --git a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift index 15c9b403ab..d50a621f91 100644 --- a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift +++ b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift @@ -24,6 +24,7 @@ import BrowserServicesKit class PrivacyConfigurationMock: PrivacyConfiguration { var identifier: String = "id" + var version: String? = "123456789" var userUnprotectedDomains: [String] = [] From 84f3ac504c4a98386c097864c85cd843e4c9db1d Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 16 Sep 2024 15:46:42 +0200 Subject: [PATCH 03/46] Release 7.138.0-0 (#3362) --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 65 ++++++++++++++++--- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++-------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- 5 files changed, 87 insertions(+), 42 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index f79c653987..c1d3187e3c 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.137.0 +MARKETING_VERSION = 7.138.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index bf0b7a567d..a029b30de7 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"9087766799743533c0741b03cea431d1\"" - public static let embeddedDataSHA = "9e9fcfd329fc587ba732cf9cb7e71d81f7af7717c3f804f28b9c8603599ee8d8" + public static let embeddedDataETag = "\"24a5ebbe8d8bb826ca87eacd9e26c281\"" + public static let embeddedDataSHA = "9c4301232c8c0429f4148e5d23c4ec065bffffdf6be14dfdb838ffd9f46b9bb5" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index 6330f1c820..9bfa01120a 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1725898107484, + "version": 1726491752161, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -398,6 +398,19 @@ "exceptions": [], "hash": "9ae27f0fb5a6f7db96b239b4def3b5b6" }, + "autofillSurveys": { + "state": "enabled", + "settings": { + "surveys": [ + { + "id": "2024-09-10", + "url": "https://selfserve.decipherinc.com/survey/selfserve/32ab/240900" + } + ] + }, + "exceptions": [], + "hash": "fa60baa3935e8ab70e77dafa347a6a71" + }, "autofill": { "exceptions": [ { @@ -465,6 +478,11 @@ }, "hash": "28d4af98382248e184c4315bd49f4222" }, + "bookmarks": { + "state": "enabled", + "exceptions": [], + "hash": "697382e31649d84b01166f1dc6f790d6" + }, "breakageReporting": { "state": "disabled", "exceptions": [ @@ -1432,6 +1450,9 @@ }, "openInNewTab": { "state": "disabled" + }, + "enableDuckPlayer": { + "state": "internal" } }, "settings": { @@ -1505,11 +1526,21 @@ "value": "enabled" } ] + }, + { + "domain": "m.youtube.com", + "patchSettings": [ + { + "op": "replace", + "path": "/overlays/youtube/state", + "value": "enabled" + } + ] } ] }, - "state": "disabled", - "hash": "9ae5b4d92ce6ba8a8c233a5c9aab93a6" + "state": "internal", + "hash": "887a8548cb01c9568e4d1b4d195af88b" }, "elementHiding": { "exceptions": [ @@ -5236,6 +5267,9 @@ { "domain": "norton.com" }, + { + "domain": "jcrew.com" + }, { "domain": "marvel.com" }, @@ -5267,7 +5301,7 @@ "privacy-test-pages.site" ] }, - "hash": "d84eb509cdbdc25d46085688260ac6fa" + "hash": "91efce5371a70d32b647ef748376666e" }, "harmfulApis": { "settings": { @@ -5473,6 +5507,11 @@ "exceptions": [], "hash": "429cea8d27316dc62af04159ec7c42b5" }, + "marketplaceAdPostback": { + "state": "enabled", + "exceptions": [], + "hash": "697382e31649d84b01166f1dc6f790d6" + }, "mediaPlaybackRequiresUserGesture": { "exceptions": [], "state": "disabled", @@ -5998,6 +6037,12 @@ "tf1info.fr" ] }, + { + "rule": "static.adsafeprotected.com/skeleton.gif", + "domains": [ + "" + ] + }, { "rule": "static.adsafeprotected.com/iasPET.1.js", "domains": [ @@ -9182,7 +9227,7 @@ "domain": "capitalone.com" } ], - "hash": "96d026a445971b650f4a6e899e339ff8" + "hash": "7c26c9953069fedb8fe6532c86cf5572" }, "trackingCookies1p": { "settings": { @@ -9408,6 +9453,11 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "windowsDownloadLink": { + "exceptions": [], + "state": "enabled", + "hash": "52857469413a66e8b0c7b00de5589162" + }, "windowsExternalPreviewReleases": { "exceptions": [], "state": "disabled", @@ -9442,11 +9492,6 @@ "exceptions": [], "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" - }, - "windowsDownloadLink": { - "exceptions": [], - "state": "enabled", - "hash": "52857469413a66e8b0c7b00de5589162" } }, "unprotectedTemporary": [] diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4fc50dc02a..18e8d0eb00 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9100,7 +9100,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9137,7 +9137,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9227,7 +9227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9254,7 +9254,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9403,7 +9403,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9428,7 +9428,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9497,7 +9497,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9531,7 +9531,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9564,7 +9564,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9594,7 +9594,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9904,7 +9904,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9935,7 +9935,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9963,7 +9963,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9996,7 +9996,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10026,7 +10026,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10059,11 +10059,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10296,7 +10296,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10323,7 +10323,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10355,7 +10355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10392,7 +10392,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10427,7 +10427,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10462,11 +10462,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10639,11 +10639,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10672,10 +10672,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 7cb7486a5e..4aebc33710 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.137.0 + 7.138.0 Key version Title From 7669a95abe22000b9b06c639b3d1d0779fa7647f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 17 Sep 2024 15:12:45 +0200 Subject: [PATCH 04/46] Decouple NTP settings persistence from AppUserDefaults (#3354) Task/Issue URL: https://app.asana.com/0/72649045549333/1208273840160821/f Tech Design URL: CC: **Description**: Extract NTP data persistence into smaller protocols and use `UserDefaultsWrapper` to implement the actual storing. The change allows for better separation of concerns and potentially storing data in a different way on a case-by-case basis. **Steps to test this PR**: 1. Verify NTP sections and shortcuts settings (order, enabled state) are preserved across app instances. 2. Verify intro message is dismissed after 3 views (it can be set up from Debug menu). **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.xcodeproj/project.pbxproj | 8 +++++ DuckDuckGo/AppSettings.swift | 6 ---- DuckDuckGo/AppUserDefaults.swift | 15 -------- DuckDuckGo/NewTabPageIntroDataStoring.swift | 34 +++++++++++++++++++ DuckDuckGo/NewTabPageIntroMessageSetup.swift | 10 +++--- DuckDuckGo/NewTabPageManager.swift | 19 ++++++++--- DuckDuckGo/NewTabPageModel.swift | 16 ++++----- DuckDuckGo/NewTabPageSectionsDebugView.swift | 26 +++++++------- .../NewTabPageSectionsSettingsStorage.swift | 2 +- .../NewTabPageSettingsPersistentStorage.swift | 17 +++++----- .../NewTabPageSettingsPersistentStore.swift | 31 +++++++++++++++++ .../NewTabPageShortcutsSettingsStorage.swift | 2 +- DuckDuckGo/NewTabPageView.swift | 6 +++- .../NewTabPageIntroMessageSetupTests.swift | 21 +++++++----- DuckDuckGoTests/NewTabPageModelTests.swift | 20 +++++------ ...NewTabPageSectionsSettingsModelTests.swift | 7 ++-- ...abPageSettingsPersistentStorageTests.swift | 8 ++--- ...ewTabPageShortcutsSettingsModelTests.swift | 3 +- 18 files changed, 161 insertions(+), 90 deletions(-) create mode 100644 DuckDuckGo/NewTabPageIntroDataStoring.swift create mode 100644 DuckDuckGo/NewTabPageSettingsPersistentStore.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 18e8d0eb00..db7fdaaf73 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -338,6 +338,8 @@ 6FD8E5202C5BA23200345670 /* NewTabPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51F2C5BA23200345670 /* NewTabPageModel.swift */; }; 6FD8E5222C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E5212C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift */; }; 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; + 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */; }; + 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */; }; 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127372C20492500EB5724 /* NewTabPage.swift */; }; @@ -1607,6 +1609,8 @@ 6FD8E51F2C5BA23200345670 /* NewTabPageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageModel.swift; sourceTree = ""; }; 6FD8E5212C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetup.swift; sourceTree = ""; }; 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; + 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroDataStoring.swift; sourceTree = ""; }; + 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStore.swift; sourceTree = ""; }; 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FE127372C20492500EB5724 /* NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPage.swift; sourceTree = ""; }; @@ -3844,6 +3848,7 @@ 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */, 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */, 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */, + 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */, ); name = Storage; sourceTree = ""; @@ -3912,6 +3917,7 @@ children = ( 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */, 6FD8E5212C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift */, + 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */, ); name = IntroMessage; sourceTree = ""; @@ -7299,6 +7305,7 @@ F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, + 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, @@ -7379,6 +7386,7 @@ 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */, 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, + 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index c0613b3fd5..d3b99acc70 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -82,14 +82,8 @@ protocol AppSettings: AnyObject, AppDebugSettings { var duckPlayerMode: DuckPlayerMode { get set } var duckPlayerAskModeOverlayHidden: Bool { get set } - - var newTabPageShortcutsSettings: Data? { get set } - var newTabPageSectionsSettings: Data? { get set } - var newTabPageIntroMessageEnabled: Bool? { get set } - var newTabPageIntroMessageSeenCount: Int { get set } } protocol AppDebugSettings { - var newTabPageSectionsEnabled: Bool { get set } var onboardingHighlightsEnabled: Bool { get set } } diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index ceb2538043..2c54a3a749 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -383,9 +383,6 @@ public class AppUserDefaults: AppSettings { userDefaults?.setValue(newValue.rawValue, forKey: Keys.crashCollectionOptInStatus) } } - - @UserDefaultsWrapper(key: .debugNewTabPageSectionsEnabledKey, defaultValue: false) - var newTabPageSectionsEnabled: Bool var duckPlayerMode: DuckPlayerMode { get { @@ -418,18 +415,6 @@ public class AppUserDefaults: AppSettings { } } - @UserDefaultsWrapper(key: .newTabPageShortcutsSettings, defaultValue: nil) - var newTabPageShortcutsSettings: Data? - - @UserDefaultsWrapper(key: .newTabPageSectionsSettings, defaultValue: nil) - var newTabPageSectionsSettings: Data? - - @UserDefaultsWrapper(key: .newTabPageIntroMessageEnabled, defaultValue: nil) - var newTabPageIntroMessageEnabled: Bool? - - @UserDefaultsWrapper(key: .newTabPageIntroMessageSeenCount, defaultValue: 0) - var newTabPageIntroMessageSeenCount: Int - @UserDefaultsWrapper(key: .debugOnboardingHighlightsEnabledKey, defaultValue: false) var onboardingHighlightsEnabled: Bool } diff --git a/DuckDuckGo/NewTabPageIntroDataStoring.swift b/DuckDuckGo/NewTabPageIntroDataStoring.swift new file mode 100644 index 0000000000..af42baa75f --- /dev/null +++ b/DuckDuckGo/NewTabPageIntroDataStoring.swift @@ -0,0 +1,34 @@ +// +// NewTabPageIntroDataStoring.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core + +protocol NewTabPageIntroDataStoring: AnyObject { + var newTabPageIntroMessageEnabled: Bool? { get set } + var newTabPageIntroMessageSeenCount: Int { get set } +} + +final class NewTabPageIntroDataUserDefaultsStorage: NewTabPageIntroDataStoring { + @UserDefaultsWrapper(key: .newTabPageIntroMessageEnabled, defaultValue: nil) + var newTabPageIntroMessageEnabled: Bool? + + @UserDefaultsWrapper(key: .newTabPageIntroMessageSeenCount, defaultValue: 0) + var newTabPageIntroMessageSeenCount: Int +} diff --git a/DuckDuckGo/NewTabPageIntroMessageSetup.swift b/DuckDuckGo/NewTabPageIntroMessageSetup.swift index 41010acdf5..5ee9a2df42 100644 --- a/DuckDuckGo/NewTabPageIntroMessageSetup.swift +++ b/DuckDuckGo/NewTabPageIntroMessageSetup.swift @@ -21,24 +21,24 @@ import BrowserServicesKit import Core struct NewTabPageIntroMessageSetup { - let appSettings: AppSettings + let storage: NewTabPageIntroDataStoring let statistics: StatisticsStore let newTabPageManager: NewTabPageManaging - init(appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + init(storage: NewTabPageIntroDataStoring = NewTabPageIntroDataUserDefaultsStorage(), statistics: StatisticsStore = StatisticsUserDefaults(), newTabPageManager: NewTabPageManaging = NewTabPageManager()) { - self.appSettings = appSettings + self.storage = storage self.statistics = statistics self.newTabPageManager = newTabPageManager } func perform() { - let isNotSetUp = appSettings.newTabPageIntroMessageEnabled == nil + let isNotSetUp = storage.newTabPageIntroMessageEnabled == nil guard newTabPageManager.isAvailableInPublicRelease && isNotSetUp else { return } // For new users we **don't** want intro message - appSettings.newTabPageIntroMessageEnabled = statistics.installDate != nil + storage.newTabPageIntroMessageEnabled = statistics.installDate != nil } } diff --git a/DuckDuckGo/NewTabPageManager.swift b/DuckDuckGo/NewTabPageManager.swift index 6a7dfa2baa..e4b6a33de7 100644 --- a/DuckDuckGo/NewTabPageManager.swift +++ b/DuckDuckGo/NewTabPageManager.swift @@ -31,17 +31,26 @@ protocol NewTabPageDebugging: NewTabPageManaging { var isFeatureFlagEnabled: Bool { get } } +protocol NewTabPageLocalFlagStoring: AnyObject { + var newTabPageSectionsEnabled: Bool { get set } +} + +final class NewTabPageLocalFlagUserDefaultsStorage: NewTabPageLocalFlagStoring { + @UserDefaultsWrapper(key: .debugNewTabPageSectionsEnabledKey, defaultValue: false) + var newTabPageSectionsEnabled: Bool +} + final class NewTabPageManager: NewTabPageManaging, NewTabPageDebugging { - var appDefaults: AppDebugSettings + let localFlagStorage: NewTabPageLocalFlagStoring let featureFlagger: FeatureFlagger let internalUserDecider: InternalUserDecider - init(appDefaults: AppDebugSettings = AppDependencyProvider.shared.appSettings, + init(localFlagStorage: NewTabPageLocalFlagStoring = NewTabPageLocalFlagUserDefaultsStorage(), featureFlager: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, internalUserDecider: InternalUserDecider = AppDependencyProvider.shared.internalUserDecider) { - self.appDefaults = appDefaults + self.localFlagStorage = localFlagStorage self.featureFlagger = featureFlager self.internalUserDecider = internalUserDecider } @@ -67,10 +76,10 @@ final class NewTabPageManager: NewTabPageManaging, NewTabPageDebugging { var isLocalFlagEnabled: Bool { get { - appDefaults.newTabPageSectionsEnabled + localFlagStorage.newTabPageSectionsEnabled } set { - appDefaults.newTabPageSectionsEnabled = newValue + localFlagStorage.newTabPageSectionsEnabled = newValue } } diff --git a/DuckDuckGo/NewTabPageModel.swift b/DuckDuckGo/NewTabPageModel.swift index f9d826133b..8cd18b0d00 100644 --- a/DuckDuckGo/NewTabPageModel.swift +++ b/DuckDuckGo/NewTabPageModel.swift @@ -26,15 +26,15 @@ final class NewTabPageModel: ObservableObject { @Published private(set) var isOnboarding: Bool @Published var isShowingSettings: Bool - private let appSettings: AppSettings + private var introDataStorage: NewTabPageIntroDataStoring private let pixelFiring: PixelFiring.Type - init(appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + init(introDataStorage: NewTabPageIntroDataStoring = NewTabPageIntroDataUserDefaultsStorage(), pixelFiring: PixelFiring.Type = Pixel.self) { - self.appSettings = appSettings + self.introDataStorage = introDataStorage self.pixelFiring = pixelFiring - isIntroMessageVisible = appSettings.newTabPageIntroMessageEnabled ?? false + isIntroMessageVisible = introDataStorage.newTabPageIntroMessageEnabled ?? false isOnboarding = false isShowingSettings = false } @@ -42,16 +42,16 @@ final class NewTabPageModel: ObservableObject { func introMessageDisplayed() { pixelFiring.fire(.newTabPageMessageDisplayed, withAdditionalParameters: [:]) - appSettings.newTabPageIntroMessageSeenCount += 1 - if appSettings.newTabPageIntroMessageSeenCount >= 3 { - appSettings.newTabPageIntroMessageEnabled = false + introDataStorage.newTabPageIntroMessageSeenCount += 1 + if introDataStorage.newTabPageIntroMessageSeenCount >= 3 { + introDataStorage.newTabPageIntroMessageEnabled = false } } func dismissIntroMessage() { pixelFiring.fire(.newTabPageMessageDismissed, withAdditionalParameters: [:]) - appSettings.newTabPageIntroMessageEnabled = false + introDataStorage.newTabPageIntroMessageEnabled = false isIntroMessageVisible = false } diff --git a/DuckDuckGo/NewTabPageSectionsDebugView.swift b/DuckDuckGo/NewTabPageSectionsDebugView.swift index 1f3a05e198..a0c4c381b0 100644 --- a/DuckDuckGo/NewTabPageSectionsDebugView.swift +++ b/DuckDuckGo/NewTabPageSectionsDebugView.swift @@ -22,8 +22,8 @@ import SwiftUI struct NewTabPageSectionsDebugView: View { private var newTabPageDebugging: NewTabPageDebugging - private var appSettings: AppSettings - + private let introDataStorage: NewTabPageIntroDataStoring + @State private var isFeatureEnabled: Bool @State private var introMessageCount: Int @State private var isIntroMessageInitialized: Bool @@ -34,24 +34,24 @@ struct NewTabPageSectionsDebugView: View { } set: { newTabPageDebugging.isLocalFlagEnabled = $0 isFeatureEnabled = newTabPageDebugging.isNewTabPageSectionsEnabled - isIntroMessageInitialized = appSettings.newTabPageIntroMessageEnabled != nil + isIntroMessageInitialized = introDataStorage.newTabPageIntroMessageEnabled != nil } } private var introMessageEnabled: Binding { Binding { - appSettings.newTabPageIntroMessageEnabled ?? false + introDataStorage.newTabPageIntroMessageEnabled ?? false } set: { - appSettings.newTabPageIntroMessageEnabled = $0 - isIntroMessageInitialized = appSettings.newTabPageIntroMessageEnabled != nil + introDataStorage.newTabPageIntroMessageEnabled = $0 + isIntroMessageInitialized = introDataStorage.newTabPageIntroMessageEnabled != nil } } private var introMessageCountBinding: Binding { Binding { - appSettings.newTabPageIntroMessageSeenCount + introDataStorage.newTabPageIntroMessageSeenCount } set: { - appSettings.newTabPageIntroMessageSeenCount = $0 + introDataStorage.newTabPageIntroMessageSeenCount = $0 introMessageCount = $0 } } @@ -60,10 +60,10 @@ struct NewTabPageSectionsDebugView: View { let manager = NewTabPageManager() newTabPageDebugging = manager isFeatureEnabled = manager.isNewTabPageSectionsEnabled - - appSettings = AppDependencyProvider.shared.appSettings - introMessageCount = appSettings.newTabPageIntroMessageSeenCount - isIntroMessageInitialized = appSettings.newTabPageIntroMessageEnabled != nil + + introDataStorage = NewTabPageIntroDataUserDefaultsStorage() + introMessageCount = introDataStorage.newTabPageIntroMessageSeenCount + isIntroMessageInitialized = introDataStorage.newTabPageIntroMessageEnabled != nil } var body: some View { @@ -134,7 +134,7 @@ struct NewTabPageSectionsDebugView: View { }) Button("Reset intro message", action: { - appSettings.newTabPageIntroMessageEnabled = nil + introDataStorage.newTabPageIntroMessageEnabled = nil introMessageCountBinding.wrappedValue = 0 isIntroMessageInitialized = false }) diff --git a/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift b/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift index 12f38c47e6..476baad6a7 100644 --- a/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift +++ b/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift @@ -28,7 +28,7 @@ typealias NewTabPageSectionsSettingsStorage = NewTabPageSettingsPersistentStorag extension NewTabPageSettingsPersistentStorage { convenience init() { - self.init(keyPath: \.newTabPageSectionsSettings, + self.init(persistentStore: NewTabPageSectionsSettingsStore(), defaultOrder: NewTabPageSection.allCases, defaultEnabledItems: NewTabPageSection.allCases) } diff --git a/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift b/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift index 9c746bccf6..c9b7bb3298 100644 --- a/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift +++ b/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift @@ -24,19 +24,20 @@ private struct NewTabPageItemSettings: Coda let enabledItems: Set } +protocol NewTabPageSettingsPersistentStore: AnyObject { + var data: Data? { get set } +} + final class NewTabPageSettingsPersistentStorage: NewTabPageSettingsStorage { private(set) var itemsOrder: [Item] private var enabledItems: Set - private var appSettings: AppSettings - private let keyPath: WritableKeyPath + private var persistentStore: any NewTabPageSettingsPersistentStore - init(appSettings: AppSettings = AppDependencyProvider.shared.appSettings, - keyPath: WritableKeyPath, + init(persistentStore: NewTabPageSettingsPersistentStore, defaultOrder: [Item], defaultEnabledItems: [Item]) { - self.appSettings = appSettings - self.keyPath = keyPath + self.persistentStore = persistentStore self.itemsOrder = defaultOrder self.enabledItems = Set(defaultEnabledItems) @@ -62,12 +63,12 @@ final class NewTabPageSettingsPersistentStorage.self, from: settingsData) { itemsOrder = settings.itemsOrder enabledItems = settings.enabledItems diff --git a/DuckDuckGo/NewTabPageSettingsPersistentStore.swift b/DuckDuckGo/NewTabPageSettingsPersistentStore.swift new file mode 100644 index 0000000000..fa3a12540f --- /dev/null +++ b/DuckDuckGo/NewTabPageSettingsPersistentStore.swift @@ -0,0 +1,31 @@ +// +// NewTabPageSettingsPersistentStore.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core + +final class NewTabPageShorctutsSettingsStore: NewTabPageSettingsPersistentStore { + @UserDefaultsWrapper(key: .newTabPageShortcutsSettings, defaultValue: nil) + var data: Data? +} + +final class NewTabPageSectionsSettingsStore: NewTabPageSettingsPersistentStore { + @UserDefaultsWrapper(key: .newTabPageSectionsSettings, defaultValue: nil) + var data: Data? +} diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift index 7560398968..f4f435d921 100644 --- a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift +++ b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift @@ -23,7 +23,7 @@ typealias NewTabPageShortcutsSettingsStorage = NewTabPageSettingsPersistentStora extension NewTabPageSettingsPersistentStorage { convenience init() { - self.init(keyPath: \.newTabPageShortcutsSettings, + self.init(persistentStore: NewTabPageShorctutsSettingsStore(), defaultOrder: NewTabPageShortcut.allCases, defaultEnabledItems: NewTabPageShortcut.enabledByDefault) } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index ecc09bbba7..e8a5b8584f 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -335,6 +335,10 @@ private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration private extension NewTabPageSectionsSettingsStorage { static func emptyStorage() -> Self { - Self.init(keyPath: \.newTabPageSectionsSettings, defaultOrder: [], defaultEnabledItems: []) + Self.init(persistentStore: EmptyStore(), defaultOrder: [], defaultEnabledItems: []) + } + + private final class EmptyStore: NewTabPageSettingsPersistentStore { + var data: Data? } } diff --git a/DuckDuckGoTests/NewTabPageIntroMessageSetupTests.swift b/DuckDuckGoTests/NewTabPageIntroMessageSetupTests.swift index 53780e2e9f..5cc797fa44 100644 --- a/DuckDuckGoTests/NewTabPageIntroMessageSetupTests.swift +++ b/DuckDuckGoTests/NewTabPageIntroMessageSetupTests.swift @@ -22,7 +22,7 @@ import XCTest final class NewTabPageIntroMessageSetupTests: XCTestCase { - private let appSettings = AppSettingsMock() + private let storage = NewTabPageIntroDataStoringMock() private let statistics = MockStatisticsStore() private let ntpManagerMock = NewTabPageManagerMock() @@ -32,7 +32,7 @@ final class NewTabPageIntroMessageSetupTests: XCTestCase { sut.perform() - XCTAssertEqual(appSettings.newTabPageIntroMessageEnabled, true) + XCTAssertEqual(storage.newTabPageIntroMessageEnabled, true) } func testDisablesFeatureForNewUser() { @@ -41,32 +41,32 @@ final class NewTabPageIntroMessageSetupTests: XCTestCase { sut.perform() - XCTAssertEqual(appSettings.newTabPageIntroMessageEnabled, false) + XCTAssertEqual(storage.newTabPageIntroMessageEnabled, false) } func testDoesNothingIfSetAlready() { let sut = createSUT() statistics.installDate = nil - appSettings.newTabPageIntroMessageEnabled = true + storage.newTabPageIntroMessageEnabled = true sut.perform() - XCTAssertEqual(appSettings.newTabPageIntroMessageEnabled, true) + XCTAssertEqual(storage.newTabPageIntroMessageEnabled, true) } func testDoesNothingIfNotPubliclyReleased() { let sut = createSUT() statistics.installDate = nil ntpManagerMock.isAvailableInPublicRelease = false - appSettings.newTabPageIntroMessageEnabled = nil + storage.newTabPageIntroMessageEnabled = nil sut.perform() - XCTAssertNil(appSettings.newTabPageIntroMessageEnabled) + XCTAssertNil(storage.newTabPageIntroMessageEnabled) } private func createSUT() -> NewTabPageIntroMessageSetup { - NewTabPageIntroMessageSetup(appSettings: appSettings, statistics: statistics, newTabPageManager: ntpManagerMock) + NewTabPageIntroMessageSetup(storage: storage, statistics: statistics, newTabPageManager: ntpManagerMock) } } @@ -74,3 +74,8 @@ private final class NewTabPageManagerMock: NewTabPageManaging { var isNewTabPageSectionsEnabled: Bool = true var isAvailableInPublicRelease: Bool = true } + +final class NewTabPageIntroDataStoringMock: NewTabPageIntroDataStoring { + var newTabPageIntroMessageEnabled: Bool? + var newTabPageIntroMessageSeenCount: Int = 0 +} diff --git a/DuckDuckGoTests/NewTabPageModelTests.swift b/DuckDuckGoTests/NewTabPageModelTests.swift index bf71f38f22..7025fed9c1 100644 --- a/DuckDuckGoTests/NewTabPageModelTests.swift +++ b/DuckDuckGoTests/NewTabPageModelTests.swift @@ -22,45 +22,45 @@ import XCTest final class NewTabPageModelTests: XCTestCase { - let appSettings = AppSettingsMock() + let introDataStorage = NewTabPageIntroDataStoringMock() override func tearDown() { PixelFiringMock.tearDown() } func testDoesNotShowIntroIfSettingUndefined() { - let sut = NewTabPageModel(appSettings: appSettings) + let sut = NewTabPageModel(introDataStorage: introDataStorage) XCTAssertFalse(sut.isIntroMessageVisible) } func testShowsIntroMessage() { - appSettings.newTabPageIntroMessageEnabled = true - let sut = NewTabPageModel(appSettings: appSettings) + introDataStorage.newTabPageIntroMessageEnabled = true + let sut = NewTabPageModel(introDataStorage: introDataStorage) XCTAssertTrue(sut.isIntroMessageVisible) } func testDisablesIntroMessageWhenDismissed() { - appSettings.newTabPageIntroMessageEnabled = true - let sut = NewTabPageModel(appSettings: appSettings) + introDataStorage.newTabPageIntroMessageEnabled = true + let sut = NewTabPageModel(introDataStorage: introDataStorage) sut.dismissIntroMessage() XCTAssertFalse(sut.isIntroMessageVisible) - XCTAssertEqual(appSettings.newTabPageIntroMessageEnabled, false) + XCTAssertEqual(introDataStorage.newTabPageIntroMessageEnabled, false) } func testDisablesIntroMessageAfterMultipleImpressions() { - appSettings.newTabPageIntroMessageEnabled = true - let sut = NewTabPageModel(appSettings: appSettings) + introDataStorage.newTabPageIntroMessageEnabled = true + let sut = NewTabPageModel(introDataStorage: introDataStorage) for _ in 1...3 { sut.introMessageDisplayed() } XCTAssertTrue(sut.isIntroMessageVisible) // We want to keep the message visible on last occurence - XCTAssertEqual(appSettings.newTabPageIntroMessageEnabled, false) + XCTAssertEqual(introDataStorage.newTabPageIntroMessageEnabled, false) } func testFiresPixelWhenIntroMessageDismissed() { diff --git a/DuckDuckGoTests/NewTabPageSectionsSettingsModelTests.swift b/DuckDuckGoTests/NewTabPageSectionsSettingsModelTests.swift index 4a7fa31fae..bcc4b14177 100644 --- a/DuckDuckGoTests/NewTabPageSectionsSettingsModelTests.swift +++ b/DuckDuckGoTests/NewTabPageSectionsSettingsModelTests.swift @@ -60,8 +60,7 @@ final class NewTabPageSectionsSettingsModelTests: XCTestCase { private func createSUT() -> NewTabPageSectionsSettingsModel { let storage = NewTabPageSectionsSettingsStorage( - appSettings: AppSettingsMock(), - keyPath: \.newTabPageSectionsSettings, + persistentStore: NewTabPageSettingsPersistentStoreMock(), defaultOrder: NewTabPageSection.allCases, defaultEnabledItems: NewTabPageSection.allCases ) @@ -69,3 +68,7 @@ final class NewTabPageSectionsSettingsModelTests: XCTestCase { return NewTabPageSectionsSettingsModel(storage: storage, pixelFiring: PixelFiringMock.self) } } + +final class NewTabPageSettingsPersistentStoreMock: NewTabPageSettingsPersistentStore { + var data: Data? +} diff --git a/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift b/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift index 30f58cf733..a54ba48fe4 100644 --- a/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift +++ b/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift @@ -23,7 +23,7 @@ import XCTest final class NewTabPageSettingsPersistentStorageTests: XCTestCase { - private var appSettings = AppSettingsMock() + private var settingsPersistentStore = NewTabPageSettingsPersistentStoreMock() func testLoadsInitialStateFromDefaults() { let sut = createSUT() @@ -35,7 +35,7 @@ final class NewTabPageSettingsPersistentStorageTests: XCTestCase { func testUsesDefaultsIfDataCorrupted() { let sut = createSUT() - appSettings[keyPath: Constant.keyPath] = "Random data".data(using: .utf8) + settingsPersistentStore.data = "Random data".data(using: .utf8) XCTAssertEqual(sut.itemsOrder, Constant.defaultItems) XCTAssertEqual(sut.enabledItems, Constant.defaultItems) @@ -90,15 +90,13 @@ final class NewTabPageSettingsPersistentStorageTests: XCTestCase { } private func createSUT(defaultOrder: [StorageItem] = Constant.defaultItems, defaultEnabledItems: [StorageItem] = Constant.defaultItems) -> NewTabPageSettingsPersistentStorage { - NewTabPageSettingsPersistentStorage(appSettings: appSettings, - keyPath: Constant.keyPath, + NewTabPageSettingsPersistentStorage(persistentStore: settingsPersistentStore, defaultOrder: defaultOrder, defaultEnabledItems: defaultEnabledItems) } private enum Constant { static let defaultItems = [StorageItem.one, .two, .three] - static let keyPath = \AppSettings.newTabPageSectionsSettings } } diff --git a/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift b/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift index 463dc48c07..012f2b3b23 100644 --- a/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift +++ b/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift @@ -54,8 +54,7 @@ final class NewTabPageShortcutsSettingsModelTests: XCTestCase { private func createSUT() -> NewTabPageShortcutsSettingsModel { let storage = NewTabPageShortcutsSettingsStorage( - appSettings: AppSettingsMock(), - keyPath: \.newTabPageShortcutsSettings, + persistentStore: NewTabPageSettingsPersistentStoreMock(), defaultOrder: NewTabPageShortcut.allCases, defaultEnabledItems: NewTabPageShortcut.allCases ) From 2905855f6ca4790587e5a9be2ecf5d5b9c632d8b Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Tue, 17 Sep 2024 10:22:02 -0500 Subject: [PATCH 05/46] Make remote config accessible to background agents (#3255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1203581873609357/1207165680693234/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. See https://github.com/duckduckgo/BrowserServicesKit/pull/947 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/ContentBlockerStoreConstants.swift | 1 + Core/EtagStorage.swift | 2 +- Core/FileStore.swift | 53 +++++-- Core/PixelEvent.swift | 8 + Core/UserDefaults+NetworkProtection.swift | 8 + DuckDuckGo.xcodeproj/project.pbxproj | 18 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/AppDelegate.swift | 3 +- DuckDuckGo/AppDependencyProvider.swift | 6 +- .../Configuration/ConfigurationManager.swift | 87 +++++++++-- .../Configuration/ConfigurationStore.swift | 8 +- DuckDuckGoTests/MockDependencyProvider.swift | 2 + .../OnboardingNavigationDelegateTests.swift | 4 + .../ConfigurationManager.swift | 102 +++++++++++++ .../ConfigurationStore.swift | 119 +++++++++++++++ ...etworkProtectionPacketTunnelProvider.swift | 20 +++ .../VPNAgentConfigurationURLProvider.swift | 31 ++++ .../VPNPrivacyConfigurationManager.swift | 137 ++++++++++++++++++ 18 files changed, 578 insertions(+), 35 deletions(-) create mode 100644 PacketTunnelProvider/NetworkProtection/ConfigurationManager.swift create mode 100644 PacketTunnelProvider/NetworkProtection/ConfigurationStore.swift create mode 100644 PacketTunnelProvider/NetworkProtection/VPNAgentConfigurationURLProvider.swift create mode 100644 PacketTunnelProvider/NetworkProtection/VPNPrivacyConfigurationManager.swift diff --git a/Core/ContentBlockerStoreConstants.swift b/Core/ContentBlockerStoreConstants.swift index 7172d9b198..011f455c6a 100644 --- a/Core/ContentBlockerStoreConstants.swift +++ b/Core/ContentBlockerStoreConstants.swift @@ -22,5 +22,6 @@ import Foundation public struct ContentBlockerStoreConstants { public static let groupName = "\(Global.groupIdPrefix).contentblocker" + public static let configurationGroupName = "\(Global.groupIdPrefix).app-configuration" } diff --git a/Core/EtagStorage.swift b/Core/EtagStorage.swift index 7e0cc1fd29..aa894e3079 100644 --- a/Core/EtagStorage.swift +++ b/Core/EtagStorage.swift @@ -31,7 +31,7 @@ public protocol BlockerListETagStorage { public struct UserDefaultsETagStorage: BlockerListETagStorage { - private let defaults = UserDefaults(suiteName: "com.duckduckgo.blocker-list.etags") + private let defaults = UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") public init() { } diff --git a/Core/FileStore.swift b/Core/FileStore.swift index 5696711dbf..0b44c2e9c0 100644 --- a/Core/FileStore.swift +++ b/Core/FileStore.swift @@ -22,16 +22,30 @@ import Configuration public class FileStore { - private let groupIdentifier: String = ContentBlockerStoreConstants.groupName + private let groupIdentifier: String = ContentBlockerStoreConstants.configurationGroupName public init() { } public func persist(_ data: Data, for configuration: Configuration) throws { - do { - try data.write(to: persistenceLocation(for: configuration), options: .atomic) - } catch { - Pixel.fire(pixel: .fileStoreWriteFailed, error: error, withAdditionalParameters: ["config": configuration.rawValue]) - throw error + let file = persistenceLocation(for: configuration) + var coordinatorError: NSError? + var writeError: Error? + + NSFileCoordinator().coordinate(writingItemAt: file, options: .forReplacing, error: &coordinatorError) { fileUrl in + do { + try data.write(to: fileUrl, options: .atomic) + } catch { + Pixel.fire(pixel: .fileStoreWriteFailed, error: error, withAdditionalParameters: ["config": configuration.rawValue]) + writeError = error + } + } + + if let writeError { + throw writeError + } + if let coordinatorError { + Pixel.fire(pixel: .fileStoreCoordinatorFailed, error: coordinatorError, withAdditionalParameters: ["config": configuration.rawValue]) + throw coordinatorError } } @@ -55,22 +69,33 @@ public class FileStore { } public func loadAsData(for configuration: Configuration) -> Data? { - do { - return try Data(contentsOf: persistenceLocation(for: configuration)) - } catch { - let nserror = error as NSError - if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { - Pixel.fire(pixel: .trackerDataCouldNotBeLoaded, error: error) + let file = persistenceLocation(for: configuration) + var data: Data? + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(readingItemAt: file, error: &coordinatorError) { fileUrl in + do { + data = try Data(contentsOf: fileUrl) + } catch { + let nserror = error as NSError + if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { + Pixel.fire(pixel: .trackerDataCouldNotBeLoaded, error: error) + } } - return nil } + + if let coordinatorError { + Pixel.fire(pixel: .fileStoreCoordinatorFailed, error: coordinatorError, withAdditionalParameters: ["config": configuration.rawValue]) + } + + return data } func hasData(for configuration: Configuration) -> Bool { FileManager.default.fileExists(atPath: persistenceLocation(for: configuration).path) } - func persistenceLocation(for configuration: Configuration) -> URL { + public func persistenceLocation(for configuration: Configuration) -> URL { let path = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) return path!.appendingPathComponent(configuration.storeKey) } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2afe5960b8..755e07e3c7 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -449,6 +449,9 @@ extension Pixel { case networkProtectionVPNConfigurationRemoved case networkProtectionVPNConfigurationRemovalFailed + case networkProtectionConfigurationInvalidPayload(configuration: Configuration) + case networkProtectionConfigurationPixelTest + // MARK: remote messaging pixels case remoteMessageShown @@ -482,6 +485,7 @@ extension Pixel { case trackerDataReloadFailed case trackerDataCouldNotBeLoaded case fileStoreWriteFailed + case fileStoreCoordinatorFailed case privacyConfigurationReloadFailed case privacyConfigurationParseFailed case privacyConfigurationCouldNotBeLoaded @@ -1234,6 +1238,9 @@ extension Pixel.Event { case .networkProtectionVPNConfigurationRemoved: return "m_netp_vpn_configuration_removed" case .networkProtectionVPNConfigurationRemovalFailed: return "m_netp_vpn_configuration_removal_failed" + case .networkProtectionConfigurationInvalidPayload(let config): return "m_netp_vpn_configuration_\(config.rawValue)_invalid_payload" + case .networkProtectionConfigurationPixelTest: return "m_netp_vpn_configuration_pixel_test" + // MARK: remote messaging pixels case .remoteMessageShown: return "m_remote_message_shown" @@ -1269,6 +1276,7 @@ extension Pixel.Event { case .trackerDataReloadFailed: return "m_d_tds_r" case .trackerDataCouldNotBeLoaded: return "m_d_tds_l" case .fileStoreWriteFailed: return "m_d_fswf" + case .fileStoreCoordinatorFailed: return "m_d_configuration_file_coordinator_error" case .privacyConfigurationReloadFailed: return "m_d_pc_r" case .privacyConfigurationParseFailed: return "m_d_pc_p" case .privacyConfigurationCouldNotBeLoaded: return "m_d_pc_l" diff --git a/Core/UserDefaults+NetworkProtection.swift b/Core/UserDefaults+NetworkProtection.swift index ac5db87a93..9c6c2b7ccd 100644 --- a/Core/UserDefaults+NetworkProtection.swift +++ b/Core/UserDefaults+NetworkProtection.swift @@ -27,6 +27,14 @@ public extension UserDefaults { } return defaults } + + static var configurationGroupDefaults: UserDefaults { + let suiteName = ContentBlockerStoreConstants.configurationGroupName + guard let defaults = UserDefaults(suiteName: suiteName) else { + fatalError("Failed to create configuration UserDefaults") + } + return defaults + } } public enum NetworkProtectionUserDefaultKeys { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index db7fdaaf73..020d779a7d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 02025664298818B200E694E7 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02025663298818B100E694E7 /* NetworkExtension.framework */; }; + 021440742C7FAB1900426724 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021440732C7FAB1900426724 /* ConfigurationManager.swift */; }; + 021440762C7FAB4100426724 /* ConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021440752C7FAB4100426724 /* ConfigurationStore.swift */; }; + 021440782C7FB21C00426724 /* VPNPrivacyConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021440772C7FB21C00426724 /* VPNPrivacyConfigurationManager.swift */; }; + 0214407A2C7FB28F00426724 /* VPNAgentConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021440792C7FB28F00426724 /* VPNAgentConfigurationURLProvider.swift */; }; 0238E44F29C0FAA100615E30 /* FindInPageIOSJSSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 0238E44E29C0FAA100615E30 /* FindInPageIOSJSSupport */; }; 025CD01025826035001CD5BB /* FingerprintUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CCF75257EAFAF001CD5BB /* FingerprintUITest.swift */; }; 026DABA428242BC80089E0B5 /* MockUserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026DABA328242BC80089E0B5 /* MockUserAgent.swift */; }; @@ -1296,6 +1300,10 @@ 02025663298818B100E694E7 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 02025668298818B200E694E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 02025669298818B200E694E7 /* PacketTunnelProvider.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PacketTunnelProvider.entitlements; sourceTree = ""; }; + 021440732C7FAB1900426724 /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; + 021440752C7FAB4100426724 /* ConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationStore.swift; sourceTree = ""; }; + 021440772C7FB21C00426724 /* VPNPrivacyConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNPrivacyConfigurationManager.swift; sourceTree = ""; }; + 021440792C7FB28F00426724 /* VPNAgentConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAgentConfigurationURLProvider.swift; sourceTree = ""; }; 025CCF75257EAFAF001CD5BB /* FingerprintUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintUITest.swift; sourceTree = ""; }; 025CCFE22582601C001CD5BB /* FingerprintingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FingerprintingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 025CCFE62582601C001CD5BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -5450,6 +5458,10 @@ children = ( EEEB80A22A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift */, EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */, + 021440732C7FAB1900426724 /* ConfigurationManager.swift */, + 021440752C7FAB4100426724 /* ConfigurationStore.swift */, + 021440772C7FB21C00426724 /* VPNPrivacyConfigurationManager.swift */, + 021440792C7FB28F00426724 /* VPNAgentConfigurationURLProvider.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -7227,9 +7239,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 021440782C7FB21C00426724 /* VPNPrivacyConfigurationManager.swift in Sources */, F1FDC9312BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, BDFF03232BA3D8E300F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, + 021440762C7FAB4100426724 /* ConfigurationStore.swift in Sources */, + 021440742C7FAB1900426724 /* ConfigurationManager.swift in Sources */, EEEB80A32A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift in Sources */, + 0214407A2C7FB28F00426724 /* VPNAgentConfigurationURLProvider.swift in Sources */, EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, F1FDC9362BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */, @@ -10893,7 +10909,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 196.0.0; + version = 196.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1fc9b47178..d61b026cd2 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" : "ae3dbec01b8b72dc2ea4c510aecbc802862eab63", - "version" : "196.0.0" + "revision" : "f7083a3c74a4aa1f6a0f4ab65265eb2f422a2cf0", + "version" : "196.1.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 50b89c4358..0a5d2886ca 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -292,7 +292,7 @@ import os.log bookmarksDatabase: bookmarksDatabase, appSettings: AppDependencyProvider.shared.appSettings, internalUserDecider: AppDependencyProvider.shared.internalUserDecider, - configurationStore: ConfigurationStore.shared, + configurationStore: AppDependencyProvider.shared.configurationStore, database: Database.shared, errorEvents: RemoteMessagingStoreErrorHandling(), remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( @@ -519,6 +519,7 @@ import os.log ContentBlocking.shared.contentBlockingManager.scheduleCompilation() AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false } + AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() AppConfigurationFetch().start { result in self.sendAppLaunchPostback() diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index a6c5e6b403..e0840dde47 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -39,6 +39,7 @@ protocol DependencyProvider { var autofillLoginSession: AutofillLoginSession { get } var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager { get } var configurationManager: ConfigurationManager { get } + var configurationStore: ConfigurationStore { get } var userBehaviorMonitor: UserBehaviorMonitor { get } var subscriptionFeatureAvailability: SubscriptionFeatureAvailability { get } var subscriptionManager: SubscriptionManager { get } @@ -69,7 +70,8 @@ class AppDependencyProvider: DependencyProvider { let autofillLoginSession = AutofillLoginSession() lazy var autofillNeverPromptWebsitesManager = AutofillNeverPromptWebsitesManager() - let configurationManager = ConfigurationManager() + let configurationManager: ConfigurationManager + let configurationStore = ConfigurationStore() let userBehaviorMonitor = UserBehaviorMonitor() @@ -96,6 +98,8 @@ class AppDependencyProvider: DependencyProvider { featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) + configurationManager = ConfigurationManager(store: configurationStore) + // MARK: - Configure Subscription let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index cf33235503..c9e494f992 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -21,10 +21,15 @@ import Foundation import Core import Configuration import BrowserServicesKit +import Persistence import Common import os.log -struct ConfigurationManager { +final class ConfigurationManager: DefaultConfigurationManager { + + private enum Constants { + static let lastConfigurationInstallDateKey = "config.last.installed" + } enum UpdateResult { case noData @@ -51,7 +56,6 @@ struct ConfigurationManager { } public static let didUpdateTrackerDependencies = NSNotification.Name(rawValue: "com.duckduckgo.configurationManager.didUpdateTrackerDependencies") - private let fetcher = ConfigurationFetcher(store: ConfigurationStore.shared, eventMapping: Self.configurationDebugEvents) private static let configurationDebugEvents = EventMapping { event, error, _, _ in let domainEvent: Pixel.Event @@ -67,7 +71,22 @@ struct ConfigurationManager { } } + override init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), + store: ConfigurationStoring = AppDependencyProvider.shared.configurationStore, + defaults: KeyValueStoring = UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { + super.init(fetcher: fetcher, store: store, defaults: defaults) + addPresenter() + subscribeToLifecycleNotifications() + } + + deinit { + removePresenter() + removeLifecycleNotifications() + } + + @discardableResult func update(isDebug: Bool = false) async -> UpdateResult { + lastUpdateTime = Date() async let didFetchAnyTrackerBlockingDependencies = fetchAndUpdateTrackerBlockingDependencies(isDebug: isDebug) async let didFetchExcludedDomains = fetchAndUpdateBloomFilterExcludedDomains() async let didFetchBloomFilter = fetchAndUpdateBloomFilter() @@ -80,6 +99,14 @@ struct ConfigurationManager { return .noData } + func loadPrivacyConfigFromDiskIfNeeded() { + let storedEtag = store.loadEtag(for: .privacyConfiguration) + let privacyManagerEtag = (ContentBlocking.shared.privacyConfigurationManager as? PrivacyConfigurationManager)?.fetchedConfigData?.etag + if let privacyManagerEtag, privacyManagerEtag != storedEtag { + updateTrackerBlockingDependencies() + } + } + @discardableResult func fetchAndUpdateTrackerBlockingDependencies(isDebug: Bool = false) async -> Bool { let didFetchAnyTrackerBlockingDependencies = await fetchTrackerBlockingDependencies(isDebug: isDebug) @@ -93,8 +120,8 @@ struct ConfigurationManager { var didFetchAnyTrackerBlockingDependencies = false var tasks = [Configuration: Task<(), Swift.Error>]() - tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet) } - tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates) } + tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet, isDebug: isDebug) } + tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates, isDebug: isDebug) } tasks[.privacyConfiguration] = Task { try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) } for (configuration, task) in tasks { @@ -110,17 +137,17 @@ struct ConfigurationManager { } private func updateTrackerBlockingDependencies() { - ContentBlocking.shared.privacyConfigurationManager.reload(etag: ConfigurationStore.shared.loadEtag(for: .privacyConfiguration), - data: ConfigurationStore.shared.loadData(for: .privacyConfiguration)) - ContentBlocking.shared.trackerDataManager.reload(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), - data: ConfigurationStore.shared.loadData(for: .trackerDataSet)) + ContentBlocking.shared.privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration)) + ContentBlocking.shared.trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet), + data: store.loadData(for: .trackerDataSet)) NotificationCenter.default.post(name: ConfigurationManager.didUpdateTrackerDependencies, object: self) } @discardableResult func fetchAndUpdateBloomFilterExcludedDomains() async -> Bool { do { - try await fetcher.fetch(.bloomFilterExcludedDomains) + try await fetcher.fetch(.bloomFilterExcludedDomains, isDebug: false) try await updateBloomFilterExclusions() return true } catch { @@ -142,10 +169,10 @@ struct ConfigurationManager { } private func updateBloomFilter() async throws { - guard let specData = ConfigurationStore.shared.loadData(for: .bloomFilterSpec) else { + guard let specData = store.loadData(for: .bloomFilterSpec) else { throw Error.bloomFilterSpecNotFound } - guard let bloomFilterData = ConfigurationStore.shared.loadData(for: .bloomFilterBinary) else { + guard let bloomFilterData = store.loadData(for: .bloomFilterBinary) else { throw Error.bloomFilterBinaryNotFound } let specification = try JSONDecoder().decode(HTTPSBloomFilterSpecification.self, from: specData) @@ -154,7 +181,7 @@ struct ConfigurationManager { } private func updateBloomFilterExclusions() async throws { - guard let excludedDomainsData = ConfigurationStore.shared.loadData(for: .bloomFilterExcludedDomains) else { + guard let excludedDomainsData = store.loadData(for: .bloomFilterExcludedDomains) else { throw Error.bloomFilterExcludedDomainsNotFound } let excludedDomains = try HTTPSUpgradeParser.convertExcludedDomainsData(excludedDomainsData) @@ -163,3 +190,39 @@ struct ConfigurationManager { } } + +extension ConfigurationManager { + override var presentedItemURL: URL? { + return store.fileUrl(for: .privacyConfiguration).deletingLastPathComponent() + } + + override func presentedSubitemDidAppear(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateTrackerBlockingDependencies() + } + + override func presentedSubitemDidChange(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateTrackerBlockingDependencies() + } + + func subscribeToLifecycleNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(addPresenter), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(removePresenter), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + func removeLifecycleNotifications() { + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + @objc + func addPresenter() { + NSFileCoordinator.addFilePresenter(self) + } + + @objc + func removePresenter() { + NSFileCoordinator.removeFilePresenter(self) + } +} diff --git a/DuckDuckGo/Configuration/ConfigurationStore.swift b/DuckDuckGo/Configuration/ConfigurationStore.swift index 4787925aaa..3b94d2a4cc 100644 --- a/DuckDuckGo/Configuration/ConfigurationStore.swift +++ b/DuckDuckGo/Configuration/ConfigurationStore.swift @@ -22,8 +22,6 @@ import Configuration import Core struct ConfigurationStore: ConfigurationStoring { - - static let shared = ConfigurationStore() private let etagStorage: BlockerListETagStorage private let fileStore: FileStore @@ -56,5 +54,9 @@ struct ConfigurationStore: ConfigurationStoring { mutating func saveEtag(_ etag: String, for configuration: Configuration) throws { etagStorage.saveEtag(etag, for: configuration) } - + + func fileUrl(for configuration: Configuration) -> URL { + return fileStore.persistenceLocation(for: configuration) + } + } diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index 8dad3f625a..f0df5f6a4a 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -38,6 +38,7 @@ class MockDependencyProvider: DependencyProvider { var autofillLoginSession: AutofillLoginSession var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager var configurationManager: ConfigurationManager + var configurationStore: ConfigurationStore var userBehaviorMonitor: UserBehaviorMonitor var subscriptionFeatureAvailability: SubscriptionFeatureAvailability var subscriptionManager: SubscriptionManager @@ -60,6 +61,7 @@ class MockDependencyProvider: DependencyProvider { downloadManager = defaultProvider.downloadManager autofillLoginSession = defaultProvider.autofillLoginSession autofillNeverPromptWebsitesManager = defaultProvider.autofillNeverPromptWebsitesManager + configurationStore = defaultProvider.configurationStore configurationManager = defaultProvider.configurationManager userBehaviorMonitor = defaultProvider.userBehaviorMonitor subscriptionFeatureAvailability = defaultProvider.subscriptionFeatureAvailability diff --git a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift index ab48dedbbc..aceb3eb7b6 100644 --- a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift +++ b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift @@ -200,6 +200,10 @@ class MockConfigurationStoring: ConfigurationStoring { func saveEtag(_ etag: String, for configuration: Configuration) throws { } + func fileUrl(for configuration: Configuration) -> URL { + return URL(string: "file:///\(configuration.rawValue)")! + } + } class MockRemoteMessagingAvailabilityProviding: RemoteMessagingAvailabilityProviding { diff --git a/PacketTunnelProvider/NetworkProtection/ConfigurationManager.swift b/PacketTunnelProvider/NetworkProtection/ConfigurationManager.swift new file mode 100644 index 0000000000..0dc1eda71f --- /dev/null +++ b/PacketTunnelProvider/NetworkProtection/ConfigurationManager.swift @@ -0,0 +1,102 @@ +// +// ConfigurationManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log +import Core +import BrowserServicesKit +import Common +import Configuration +import Persistence + +final class ConfigurationManager: DefaultConfigurationManager { + + static let configurationDebugEvents = EventMapping { event, error, _, _ in + let domainEvent: Pixel.Event + switch event { + case .invalidPayload(let configuration): + domainEvent = .networkProtectionConfigurationInvalidPayload(configuration: configuration) + } + + Pixel.fire(pixel: domainEvent, error: error) + } + + override init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), + store: ConfigurationStoring = ConfigurationStore(), + defaults: KeyValueStoring = UserDefaults.configurationGroupDefaults) { + super.init(fetcher: fetcher, store: store, defaults: defaults) + } + + func log() { + Logger.config.log("last update \(String(describing: self.lastUpdateTime), privacy: .public)") + Logger.config.log("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)") + } + + override public func refreshNow(isDebug: Bool = false) async { + let updateConfigDependenciesTask = Task { + let didFetchConfig = await fetchConfigDependencies(isDebug: isDebug) + if didFetchConfig { + updateConfigDependencies() + tryAgainLater() + } + } + + await updateConfigDependenciesTask.value + + (store as? ConfigurationStore)?.log() + log() + } + + func fetchConfigDependencies(isDebug: Bool) async -> Bool { + do { + try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) + return true + } catch { + Logger.config.error( + "Failed to complete configuration update to \(Configuration.privacyConfiguration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) + tryAgainSoon() + } + + return false + } + + func updateConfigDependencies() { + VPNPrivacyConfigurationManager.shared.reload( + etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration) + ) + } +} + +extension ConfigurationManager { + override var presentedItemURL: URL? { + store.fileUrl(for: .privacyConfiguration).deletingLastPathComponent() + } + + override func presentedSubitemDidAppear(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateConfigDependencies() + } + + override func presentedSubitemDidChange(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateConfigDependencies() + } +} diff --git a/PacketTunnelProvider/NetworkProtection/ConfigurationStore.swift b/PacketTunnelProvider/NetworkProtection/ConfigurationStore.swift new file mode 100644 index 0000000000..5a1d612c37 --- /dev/null +++ b/PacketTunnelProvider/NetworkProtection/ConfigurationStore.swift @@ -0,0 +1,119 @@ +// +// ConfigurationStore.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log +import Core +import Configuration +import Persistence + +struct ConfigurationStore: ConfigurationStoring { + + enum Error: Swift.Error { + case unsupportedConfig + } + + private var defaults: KeyValueStoring + + private var privacyConfigurationEtagKey: String { + return "privacyConfiguration" + } + private var privacyConfigurationEtag: String? { + get { + defaults.object(forKey: privacyConfigurationEtagKey) as? String + } + set { + defaults.set(newValue, forKey: privacyConfigurationEtagKey) + } + } + + init(defaults: KeyValueStoring = UserDefaults.configurationGroupDefaults) { + self.defaults = defaults + } + + func log() { + Logger.config.log("privacyConfigurationEtag \(self.privacyConfigurationEtag ?? "", privacy: .public)") + } + + func loadData(for configuration: Configuration) -> Data? { + let file = fileUrl(for: configuration) + var data: Data? + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(readingItemAt: file, error: &coordinatorError) { fileUrl in + do { + data = try Data(contentsOf: fileUrl) + } catch { + let nserror = error as NSError + + if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { + Pixel.fire(pixel: .trackerDataCouldNotBeLoaded, error: error, withAdditionalParameters: ["target": "vpn"]) + } + } + } + + if let coordinatorError { + Logger.config.error("Unable to read \(configuration.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } + + return data + } + + func loadEtag(for configuration: Configuration) -> String? { + if configuration == .privacyConfiguration { + return privacyConfigurationEtag + } + + return nil + } + + func loadEmbeddedEtag(for configuration: Configuration) -> String? { + // If we ever need the full embedded config, we need to return its etag here + return nil + } + + mutating func saveData(_ data: Data, for configuration: Configuration) throws { + guard configuration == .privacyConfiguration else { throw Error.unsupportedConfig } + let file = fileUrl(for: configuration) + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(writingItemAt: file, options: .forReplacing, error: &coordinatorError) { fileUrl in + do { + try data.write(to: fileUrl, options: .atomic) + } catch { + Logger.config.error("Unable to write \(configuration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + } + + if let coordinatorError { + Logger.config.error("Unable to write \(configuration.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } + } + + mutating func saveEtag(_ etag: String, for configuration: Configuration) throws { + guard configuration == .privacyConfiguration else { throw Error.unsupportedConfig } + + privacyConfigurationEtag = etag + } + + func fileUrl(for configuration: Configuration) -> URL { + let path = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "\(Global.groupIdPrefix).app-configuration") + return path!.appendingPathComponent(configuration.storeKey) + } +} diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index b64d694bb6..9704c74e4e 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -19,6 +19,7 @@ import Foundation import Common +import Configuration import Combine import Core import Networking @@ -27,6 +28,7 @@ import NetworkProtection import Subscription import WidgetKit import WireGuard +import BrowserServicesKit // Initial implementation for initial Network Protection tests. Will be fleshed out with https://app.asana.com/0/1203137811378537/1204630829332227/f final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { @@ -35,6 +37,10 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { private var cancellables = Set() private let accountManager: AccountManager + private let configurationStore = ConfigurationStore() + private let configurationManager: ConfigurationManager + private var configuationSubscription: AnyCancellable? + // MARK: - PacketTunnelProvider.Event reporting private static var packetTunnelProviderEvents: EventMapping = .init { event, _, _, _ in @@ -322,6 +328,20 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + Configuration.setURLProvider(VPNAgentConfigurationURLProvider()) + configurationManager = ConfigurationManager(store: configurationStore) + configurationManager.start() + let privacyConfigurationManager = VPNPrivacyConfigurationManager.shared + // Load cached config (if any) + privacyConfigurationManager.reload(etag: configurationStore.loadEtag(for: .privacyConfiguration), data: configurationStore.loadData(for: .privacyConfiguration)) + + configuationSubscription = privacyConfigurationManager.updatesPublisher + .sink { + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(BackgroundAgentPixelTestSubfeature.pixelTest) { + DailyPixel.fire(pixel: .networkProtectionConfigurationPixelTest) + } + } + // Align Subscription environment to the VPN environment var subscriptionEnvironment = SubscriptionEnvironment.default switch settings.selectedEnvironment { diff --git a/PacketTunnelProvider/NetworkProtection/VPNAgentConfigurationURLProvider.swift b/PacketTunnelProvider/NetworkProtection/VPNAgentConfigurationURLProvider.swift new file mode 100644 index 0000000000..79ef1122a4 --- /dev/null +++ b/PacketTunnelProvider/NetworkProtection/VPNAgentConfigurationURLProvider.swift @@ -0,0 +1,31 @@ +// +// VPNAgentConfigurationURLProvider.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core +import BrowserServicesKit +import Configuration + +struct VPNAgentConfigurationURLProvider: ConfigurationURLProviding { + func url(for configuration: Configuration) -> URL { + guard configuration == .privacyConfiguration else { fatalError("\(configuration.rawValue) is not supported on this target") } + + return URL.privacyConfig + } +} diff --git a/PacketTunnelProvider/NetworkProtection/VPNPrivacyConfigurationManager.swift b/PacketTunnelProvider/NetworkProtection/VPNPrivacyConfigurationManager.swift new file mode 100644 index 0000000000..b2a3efac3d --- /dev/null +++ b/PacketTunnelProvider/NetworkProtection/VPNPrivacyConfigurationManager.swift @@ -0,0 +1,137 @@ +// +// VPNPrivacyConfigurationManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core +import BrowserServicesKit +import Combine +import Common + +public final class VPNPrivacyConfigurationManager: PrivacyConfigurationManaging { + + static let shared = VPNPrivacyConfigurationManager() + + private let lock = NSLock() + + var embeddedConfigData: Data { + let configString = """ + { + "readme": "https://github.com/duckduckgo/privacy-configuration", + "version": 1693838894358, + "features": { + "networkProtection": { + "state": "enabled", + "exceptions": [], + "settings": {} + } + }, + "unprotectedTemporary": [] + } + """ + let data = configString.data(using: .utf8) + return data! + } + + private var _fetchedConfigData: PrivacyConfigurationManager.ConfigurationData? + private(set) public var fetchedConfigData: PrivacyConfigurationManager.ConfigurationData? { + get { + lock.lock() + let data = _fetchedConfigData + lock.unlock() + return data + } + set { + lock.lock() + _fetchedConfigData = newValue + lock.unlock() + } + } + + public var currentConfig: Data { + if let fetchedData = fetchedConfigData { + return fetchedData.rawData + } + return embeddedConfigData + } + + private let updatesSubject = PassthroughSubject() + public var updatesPublisher: AnyPublisher { + updatesSubject.eraseToAnyPublisher() + } + + public var privacyConfig: BrowserServicesKit.PrivacyConfiguration { + guard let privacyConfigurationData = try? PrivacyConfigurationData(data: currentConfig) else { + fatalError("Could not retrieve privacy configuration data") + } + let privacyConfig = privacyConfiguration(withData: privacyConfigurationData, + internalUserDecider: internalUserDecider) + return privacyConfig + } + + public var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) + + @discardableResult + public func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + let result: PrivacyConfigurationManager.ReloadResult + + if let etag = etag, let data = data { + result = .downloaded + + do { + let configData = try PrivacyConfigurationData(data: data) + fetchedConfigData = (data, configData, etag) + updatesSubject.send(()) + } catch { + Pixel.fire(pixel: .trackerDataParseFailed, error: error, withAdditionalParameters: ["target": "vpn"]) + fetchedConfigData = nil + return .embeddedFallback + } + } else { + fetchedConfigData = nil + result = .embedded + } + + return result + } +} + +func privacyConfiguration(withData data: PrivacyConfigurationData, + internalUserDecider: InternalUserDecider) -> PrivacyConfiguration { + let domain = MockDomainsProtectionStore() + return AppPrivacyConfiguration(data: data, + identifier: UUID().uuidString, + localProtection: domain, + internalUserDecider: internalUserDecider) +} + +final class MockDomainsProtectionStore: DomainsProtectionStore { + var unprotectedDomains = Set() + + func disableProtection(forDomain domain: String) { + unprotectedDomains.insert(domain) + } + + func enableProtection(forDomain domain: String) { + unprotectedDomains.remove(domain) + } +} + +final class InternalUserDeciderStoreMock: InternalUserStoring { + var isInternalUser: Bool = false +} From 6b999a6dac348294ec4719a0ebeec4c929738a88 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 17 Sep 2024 23:39:46 +0200 Subject: [PATCH 06/46] Subscription feature tests (#3352) Task/Issue URL: https://app.asana.com/0/1200019156869587/1207743313459997/f **Description**: Client side iOS Subscription feature tests. --- Core/DailyPixel.swift | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 22 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- ...workProtectionFeatureVisibilityTests.swift | 2 +- .../StorePurchaseManagerTests.swift | 190 +++ .../Subscription/Subscription.storekit | 0 .../SubscriptionFeatureAvailabilityMock.swift | 34 + ...tionPagesUseSubscriptionFeatureTests.swift | 1043 ++++++++++++++++- 8 files changed, 1278 insertions(+), 21 deletions(-) create mode 100644 DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift rename {DuckDuckGo => DuckDuckGoTests}/Subscription/Subscription.storekit (100%) create mode 100644 DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift diff --git a/Core/DailyPixel.swift b/Core/DailyPixel.swift index db0b5f0b33..98d0f037e9 100644 --- a/Core/DailyPixel.swift +++ b/Core/DailyPixel.swift @@ -43,7 +43,7 @@ public final class DailyPixel { } - private static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)! + public static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)! /// Sends a given Pixel once per day. /// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected. diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 020d779a7d..0fcf5016ef 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -68,6 +68,8 @@ 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */; }; 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4B27B6A4CB00961E25 /* URLFileExtension.swift */; }; 1E4DCF4E27B6A69600961E25 /* DownloadsListHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4D27B6A69600961E25 /* DownloadsListHostingController.swift */; }; + 1E4E6C552C775B400059C0FA /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; + 1E4E6C572C78B8540059C0FA /* StorePurchaseManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */; }; 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4F4A59297193DE00625985 /* MainViewController+CookiesManaged.swift */; }; 1E4FAA6427D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */; }; 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */; }; @@ -100,6 +102,7 @@ 1E9529A12C4E748B006E80D4 /* UINavigationControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9529A02C4E748B006E80D4 /* UINavigationControllerExtension.swift */; }; 1EA51376286596A000493C6A /* PrivacyIconLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA51375286596A000493C6A /* PrivacyIconLogic.swift */; }; 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA513772866039400493C6A /* TrackerAnimationLogic.swift */; }; + 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */; }; 1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EC458452948932500CB2B13 /* UIHostingControllerExtension.swift */; }; 1EDE39D22705D4A200C99C72 /* FileSizeDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDE39D12705D4A100C99C72 /* FileSizeDebugViewController.swift */; }; 1EE411F12857C3640003FE64 /* TrackerAnimationImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE411F02857C3640003FE64 /* TrackerAnimationImageProvider.swift */; }; @@ -1355,11 +1358,13 @@ 1E1D8B6929953CE300C96994 /* autoconsent-test-page-banner.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "autoconsent-test-page-banner.html"; sourceTree = ""; }; 1E24295D293F57FA00584836 /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "cookie-icon-animated-40-light.json"; sourceTree = ""; }; + 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeatureAvailabilityMock.swift; sourceTree = ""; }; 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListViewModel.swift; sourceTree = ""; }; 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListModel.swift; sourceTree = ""; }; 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListRepresentable.swift; sourceTree = ""; }; 1E4DCF4B27B6A4CB00961E25 /* URLFileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFileExtension.swift; sourceTree = ""; }; 1E4DCF4D27B6A69600961E25 /* DownloadsListHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListHostingController.swift; sourceTree = ""; }; + 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePurchaseManagerTests.swift; sourceTree = ""; }; 1E4F4A59297193DE00625985 /* MainViewController+CookiesManaged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+CookiesManaged.swift"; sourceTree = ""; }; 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDownloadRowViewModel.swift; sourceTree = ""; }; 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteDownloadRowViewModel.swift; sourceTree = ""; }; @@ -3312,6 +3317,14 @@ name = Autoconsent; sourceTree = ""; }; + 1E25D5302C921246004400F0 /* Mocks */ = { + isa = PBXGroup; + children = ( + 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */, + ); + name = Mocks; + sourceTree = ""; + }; 1E4DCF4227B6A29D00961E25 /* View */ = { isa = PBXGroup; children = ( @@ -5296,7 +5309,6 @@ D60170BB2BA32DD6001911B5 /* Subscription.swift */, 1E53508E2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift */, D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, - D664C7952B289AA000CBFA76 /* Subscription.storekit */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, D664C7B02B289AA000CBFA76 /* UserScripts */, @@ -6082,10 +6094,13 @@ F1BDDBFC2C340D9C00459306 /* Subscription */ = { isa = PBXGroup; children = ( + 1E25D5302C921246004400F0 /* Mocks */, + D664C7952B289AA000CBFA76 /* Subscription.storekit */, BDE219E92C457B46005D5884 /* PrivacyProDataReporterTests.swift */, F1BDDBF92C340D9C00459306 /* SubscriptionContainerViewModelTests.swift */, F1BDDBFA2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift */, F1BDDBFB2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift */, + 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */, ); path = Subscription; sourceTree = ""; @@ -7018,6 +7033,7 @@ files = ( EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */, 8524092D2C77EF7A00CB28FC /* mobile_segments_test_cases.json in Resources */, + 1E4E6C552C775B400059C0FA /* Subscription.storekit in Resources */, F17843E91F36226700390DCD /* MockFiles in Resources */, 8572298A2BBEF0C800E2E802 /* AppRatingPrompt_v1 in Resources */, ); @@ -7899,6 +7915,7 @@ 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */, 9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */, 85E065C22C73ADE600D73E2A /* UsageSegmentationTests.swift in Sources */, + 1E4E6C572C78B8540059C0FA /* StorePurchaseManagerTests.swift in Sources */, 1E722729292EB24D003B5F53 /* AppSettingsMock.swift in Sources */, 8536A1C8209AF2410050739E /* MockVariantManager.swift in Sources */, C1B7B53428944EFA0098FD6A /* CoreDataTestUtilities.swift in Sources */, @@ -7949,6 +7966,7 @@ BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, 0283A2042C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, + 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, 8341D807212D5E8D000514C2 /* HashExtensionTest.swift in Sources */, C1D21E2F293A599C006E5A05 /* AutofillLoginSessionTests.swift in Sources */, 85D2187924BF6B8B004373D2 /* FaviconSourcesProviderTests.swift in Sources */, @@ -10909,7 +10927,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 196.1.0; + version = 196.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 d61b026cd2..cf5b1aae12 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" : "f7083a3c74a4aa1f6a0f4ab65265eb2f422a2cf0", - "version" : "196.1.0" + "revision" : "32a2ec64385543ccfbaaafbfe9545543a2c06aac", + "version" : "196.2.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift index 1ccbd0f91a..04b77fcfa8 100644 --- a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift +++ b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift @@ -64,7 +64,7 @@ struct NetworkProtectionFeatureVisibilityMocks: NetworkProtectionFeatureVisibili init(with options: Options) { self.options = options - let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionAppGroup = "NetworkProtectionFeatureVisibilityTests" let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, diff --git a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift new file mode 100644 index 0000000000..d8f38e823f --- /dev/null +++ b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift @@ -0,0 +1,190 @@ +// +// StorePurchaseManagerTests.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 Subscription +import SubscriptionTestingUtilities +import StoreKitTest + +final class StorePurchaseManagerTests: XCTestCase { + + private struct Constants { + static let externalID = UUID().uuidString + static let monthlySubscriptionID = "ios.subscription.1month" + static let yearlySubscriptionID = "ios.subscription.1year" + } + + var session: SKTestSession! + var storePurchaseManager: StorePurchaseManager! + + override func setUpWithError() throws { + let path = Bundle.main.url(forResource: "Subscription", withExtension: "storekit") + + session = try SKTestSession(contentsOf: path!) + session.resetToDefaultState() + session.disableDialogs = true + session.clearTransactions() + + storePurchaseManager = DefaultStorePurchaseManager() + } + + override func tearDownWithError() throws { + storePurchaseManager = nil + session = nil + } + + func testSubscriptionOptionsWhenNoCachedProducts() async throws { + // When + let subscriptionOptions = await storePurchaseManager.subscriptionOptions() + + // Then + XCTAssertNil(subscriptionOptions) + XCTAssertFalse(storePurchaseManager.areProductsAvailable) + } + + func testSubscriptionOptionsWhenAvailableProductsWereUpdated() async throws { + // Given + await storePurchaseManager.updateAvailableProducts() + + // When + guard let subscriptionOptions = await storePurchaseManager.subscriptionOptions() else { + XCTFail("Expected subscription options") + return + } + + // Then + XCTAssertEqual(subscriptionOptions.options.count, 2) + XCTAssertEqual(subscriptionOptions.features.count, SubscriptionFeatureName.allCases.count) + XCTAssertTrue(storePurchaseManager.areProductsAvailable) + + let optionIDs = subscriptionOptions.options.map { $0.id } + XCTAssertTrue(optionIDs.contains(Constants.monthlySubscriptionID)) + XCTAssertTrue(optionIDs.contains(Constants.yearlySubscriptionID)) + } + + func testHasActiveSubscriptionIsFalseWithoutPurchase() async throws { + // When + let hasActiveSubscription = await storePurchaseManager.hasActiveSubscription() + + // Then + XCTAssertFalse(hasActiveSubscription) + } + + func testPurchaseSubscription() async throws { + // Given + await storePurchaseManager.updateAvailableProducts() + + XCTAssertEqual(storePurchaseManager.purchasedProductIDs, []) + + // When + let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: Constants.externalID) + + // Then + switch result { + case .success: + XCTAssertTrue(storePurchaseManager.purchaseQueue.isEmpty) + XCTAssertEqual(storePurchaseManager.purchasedProductIDs, [Constants.yearlySubscriptionID]) + + let transactions = await StoreKitHelpers.currentEntitlements() + XCTAssertEqual(transactions.count, 1) + XCTAssertEqual(transactions.first!.appAccountToken?.uuidString, Constants.externalID) + + let hasActiveSubscription = await storePurchaseManager.hasActiveSubscription() + XCTAssertTrue(hasActiveSubscription) + case .failure: + XCTFail("Unexpected failure") + } + } + + func testPurchaseSubscriptionFailureWithoutValidProductID() async throws { + // Given + await storePurchaseManager.updateAvailableProducts() + + // When + let result = await storePurchaseManager.purchaseSubscription(with: "", externalID: Constants.externalID) + + // Then + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, StorePurchaseManagerError.productNotFound) + } + } + + func testPurchaseSubscriptionFailureWithoutValidUUID() async throws { + // Given + await storePurchaseManager.updateAvailableProducts() + + let invalidUUID = "a" + XCTAssertNil(UUID(uuidString: invalidUUID)) + + // When + let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: invalidUUID) + + // Then + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, StorePurchaseManagerError.externalIDisNotAValidUUID) + } + } + + @available(iOS 17.0, *) + func testPurchaseSubscriptionFailure() async throws { + // Given + try? await session.setSimulatedError(SKTestFailures.Purchase.purchase(.productUnavailable), + forAPI: StoreKitPurchaseAPI.purchase) + + await storePurchaseManager.updateAvailableProducts() + + // When + let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: Constants.externalID) + + // Then + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, StorePurchaseManagerError.purchaseFailed) + } + } +} + +private final class StoreKitHelpers { + + static func currentEntitlements() async -> [Transaction] { + return await Transaction.currentEntitlements.compactMap { result in + try? checkVerified(result) + }.reduce(into: [], { $0.append($1) }) + } + + static func checkVerified(_ result: VerificationResult) throws -> T { + // Check whether the JWS passes StoreKit verification. + switch result { + case .unverified: + // StoreKit parses the JWS, but it fails verification. + throw StoreError.failedVerification + case .verified(let safe): + // The result is verified. Return the unwrapped value. + return safe + } + } +} diff --git a/DuckDuckGo/Subscription/Subscription.storekit b/DuckDuckGoTests/Subscription/Subscription.storekit similarity index 100% rename from DuckDuckGo/Subscription/Subscription.storekit rename to DuckDuckGoTests/Subscription/Subscription.storekit diff --git a/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift new file mode 100644 index 0000000000..70d007616f --- /dev/null +++ b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift @@ -0,0 +1,34 @@ +// +// SubscriptionFeatureAvailabilityMock.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import BrowserServicesKit + +public final class SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { + public var isFeatureAvailable: Bool + public var isSubscriptionPurchaseAllowed: Bool + public var usesUnifiedFeedbackForm: Bool + + public init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { + self.isFeatureAvailable = isFeatureAvailable + self.isSubscriptionPurchaseAllowed = isSubscriptionPurchaseAllowed + self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm + } + +} diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 0cb08f6fac..a3b80166f4 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -19,28 +19,1043 @@ import XCTest @testable import DuckDuckGo +@testable import Core @testable import Subscription import SubscriptionTestingUtilities +import Common +import WebKit +import BrowserServicesKit +import OHHTTPStubs +import OHHTTPStubsSwift +import os.log final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { + private struct Constants { + static let userDefaultsSuiteName = "SubscriptionPagesUseSubscriptionFeatureTests" + + static let authToken = UUID().uuidString + static let accessToken = UUID().uuidString + static let externalID = UUID().uuidString + + static let email = "dax@duck.com" + + static let entitlements = [Entitlement(product: .dataBrokerProtection), + Entitlement(product: .identityTheftRestoration), + Entitlement(product: .networkProtection)] + + static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" + + static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios.rawValue, + options: [ + SubscriptionOption(id: "1", + cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), + SubscriptionOption(id: "2", + cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) + ], + features: [ + SubscriptionFeature(name: "vpn"), + SubscriptionFeature(name: "personal-information-removal"), + SubscriptionFeature(name: "identity-theft-restoration") + ]) + + static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, + entitlements: Constants.entitlements, + externalID: Constants.externalID)) + + static let mockParams: [String: String] = [:] + @MainActor static let mockScriptMessage = MockWKScriptMessage(name: "", body: "", webView: WKWebView() ) + + static let invalidTokenError = APIServiceError.serverError(statusCode: 401, error: "invalid_token") + } + + var userDefaults: UserDefaults! + + var accountStorage: AccountKeychainStorageMock! + var accessTokenStorage: SubscriptionTokenKeychainStorageMock! + var entitlementsCache: UserDefaultsCache<[Entitlement]>! + + var subscriptionService: SubscriptionEndpointServiceMock! + var authService: AuthEndpointServiceMock! + + var storePurchaseManager: StorePurchaseManagerMock! + var subscriptionEnvironment: SubscriptionEnvironment! + + var appStorePurchaseFlow: AppStorePurchaseFlow! + var appStoreRestoreFlow: AppStoreRestoreFlow! + var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! + + var accountManager: AccountManager! + var subscriptionManager: SubscriptionManager! + + var feature: SubscriptionPagesUseSubscriptionFeature! + + var pixelsFired: [String] = [] + override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + // Pixels + Pixel.isDryRun = false + stub(condition: isHost("improving.duckduckgo.com")) { request -> HTTPStubsResponse in + if let path = request.url?.path { + let pixelName = path.dropping(prefix: "/t/") + .dropping(suffix: "_ios_phone") + .dropping(suffix: "_ios_tablet") + self.pixelsFired.append(pixelName) + } + + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) + } + + // Reset all daily pixel storage + [Pixel.storage, DailyPixel.storage, UniquePixel.storage].forEach { storage in + storage.dictionaryRepresentation().keys.forEach(storage.removeObject(forKey:)) + } + + // Mocks + subscriptionService = SubscriptionEndpointServiceMock() + authService = AuthEndpointServiceMock() + + storePurchaseManager = StorePurchaseManagerMock() + subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore) + accountStorage = AccountKeychainStorageMock() + accessTokenStorage = SubscriptionTokenKeychainStorageMock() + + userDefaults = UserDefaults(suiteName: Constants.userDefaultsSuiteName)! + userDefaults.removePersistentDomain(forName: Constants.userDefaultsSuiteName) + + entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: userDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + + // Real AccountManager + accountManager = DefaultAccountManager(storage: accountStorage, + accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService) + + // Real Flows + appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, + storePurchaseManager: storePurchaseManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService) + + appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionService, + storePurchaseManager: storePurchaseManager, + accountManager: accountManager, + appStoreRestoreFlow: appStoreRestoreFlow, + authEndpointService: authService) + + appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: authService, + storePurchaseManager: storePurchaseManager, + accountManager: accountManager) + // Real SubscriptionManager + subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, + accountManager: accountManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService, + subscriptionEnvironment: subscriptionEnvironment) + + feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, + subscriptionAttributionOrigin: nil, + appStorePurchaseFlow: appStorePurchaseFlow, + appStoreRestoreFlow: appStoreRestoreFlow, + appStoreAccountManagementFlow: appStoreAccountManagementFlow) } override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - let appStorePurchaseFlow = AppStorePurchaseFlowMock(purchaseSubscriptionResult: .success("TransactionJWS"), - completeSubscriptionPurchaseResult: .success(PurchaseUpdate(type: "t", token: "t"))) - let appStoreAccountManagementFlow = AppStoreAccountManagementFlowMock(refreshAuthTokenIfNeededResult: .success("Something")) - let feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: SubscriptionMockFactory.subscriptionManager, - subscriptionAttributionOrigin: "???", - appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: SubscriptionMockFactory.appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) - // To be implemented + Pixel.isDryRun = true + pixelsFired.removeAll() + HTTPStubs.removeAllStubs() + + AppDependencyProvider.shared = AppDependencyProvider() + + subscriptionService = nil + authService = nil + storePurchaseManager = nil + subscriptionEnvironment = nil + + userDefaults = nil + + accountStorage = nil + accessTokenStorage = nil + + entitlementsCache.reset() + entitlementsCache = nil + + accountManager = nil + + // Real Flows + appStorePurchaseFlow = nil + appStoreRestoreFlow = nil + appStoreAccountManagementFlow = nil + + subscriptionManager = nil + + feature = nil + } + + // MARK: - Tests for getSubscription + + func testGetSubscriptionSuccessRefreshingAuthToken() async throws { + // Given + ensureUserAuthenticatedState() + + let newAuthToken = UUID().uuidString + + authService.validateTokenResult = .failure(Constants.invalidTokenError) + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS + authService.storeLoginResult = .success(StoreLoginResponse(authToken: newAuthToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, status: "authenticated")) + + // When + let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], newAuthToken) + XCTAssertEqual(accountManager.authToken, newAuthToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { + // Given + ensureUserAuthenticatedState() + + authService.validateTokenResult = .success(Constants.validateTokenResponse) + + // When + let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.authToken) + XCTAssertEqual(accountManager.authToken, Constants.authToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { + // Given + ensureUserUnauthenticatedState() + + authService.validateTokenResult = .failure(Constants.invalidTokenError) + storePurchaseManager.mostRecentTransactionResult = nil + + // When + let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], SubscriptionPagesUseSubscriptionFeature.Constants.empty) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + // MARK: - Tests for getSubscriptionOptions + + func testGetSubscriptionOptionsSuccess() async throws { + // Given + storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions + + // When + let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) + + XCTAssertEqual(subscriptionOptionsResult, Constants.subscriptionOptions) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { + // Given + storePurchaseManager.subscriptionOptionsResult = nil + + // When + let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) + XCTAssertEqual(subscriptionOptionsResult, SubscriptionOptions.empty) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testGetSubscriptionOptionsReturnsEmptyOptionsWhenPurchaseNotAllowed() async throws { + // Given + let mockDependencyProvider = MockDependencyProvider() + mockDependencyProvider.subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock(isFeatureAvailable: true, + isSubscriptionPurchaseAllowed: false, + usesUnifiedFeedbackForm: true) + AppDependencyProvider.shared = mockDependencyProvider + + storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions + + // When + let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) + XCTAssertEqual(subscriptionOptionsResult, SubscriptionOptions.empty) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + // MARK: - Tests for subscriptionSelected + + func testSubscriptionSelectedSuccessWhenPurchasingFirstTime() async throws { + // Given + ensureUserUnauthenticatedState() + + XCTAssertFalse(accountManager.isUserAuthenticated) + + storePurchaseManager.hasActiveSubscriptionResult = false + storePurchaseManager.mostRecentTransactionResult = nil + + authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, + externalID: Constants.externalID, + status: "created")) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) + subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, + entitlements: Constants.entitlements, + subscription: SubscriptionMockFactory.subscription)) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) + } + + func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredAppleSubscription() async throws { + // Given + ensureUserAuthenticatedState() + + XCTAssertTrue(accountManager.isUserAuthenticated) + + storePurchaseManager.hasActiveSubscriptionResult = false + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + + authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, + status: "authenticated")) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) + subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, + entitlements: Constants.entitlements, + subscription: SubscriptionMockFactory.subscription)) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertFalse(authService.createAccountCalled) + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) + } + + func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredStripeSubscription() async throws { + // Given + ensureUserAuthenticatedState() + + XCTAssertTrue(accountManager.isUserAuthenticated) + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) + subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, + entitlements: Constants.entitlements, + subscription: SubscriptionMockFactory.subscription)) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertFalse(authService.createAccountCalled) + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) + } + + func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = true + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .hasActiveSubscription) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) + } + + func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { + // Given + ensureUserUnauthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = true + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .hasActiveSubscription) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) + } + + func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { + // Given + ensureUserUnauthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + storePurchaseManager.mostRecentTransactionResult = nil + + authService.createAccountResult = .failure(Constants.invalidTokenError) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .accountCreationFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseCancelledByUser) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .cancelledByUser) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorWhenProductNotFound() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.productNotFound) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseFailed) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionCannotBeVerified) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionPendingAuthentication) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { + // Given + ensureUserAuthenticatedState() + + storePurchaseManager.hasActiveSubscriptionResult = false + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.unknownError) + + // When + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } + + // MARK: - Tests for setSubscription + + func testSetSubscriptionSuccess() async throws { + // Given + ensureUserUnauthenticatedState() + + authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + + let onSetSubscriptionCalled = expectation(description: "onSetSubscription") + feature.onSetSubscription = { + onSetSubscriptionCalled.fulfill() + } + + // When + let setSubscriptionParams = ["token": Constants.authToken] + let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertEqual(accountManager.authToken, Constants.authToken) + XCTAssertEqual(accountManager.accessToken, Constants.accessToken) + XCTAssertEqual(accountManager.email, Constants.email) + XCTAssertEqual(accountManager.externalID, Constants.externalID) + + await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { + // Given + ensureUserUnauthenticatedState() + + authService.getAccessTokenResult = .failure(Constants.invalidTokenError) + + let onSetSubscriptionCalled = expectation(description: "onSetSubscription") + onSetSubscriptionCalled.isInverted = true + feature.onSetSubscription = { + onSetSubscriptionCalled.fulfill() + } + + // When + let setSubscriptionParams = ["token": Constants.authToken] + let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertNil(accountManager.authToken) + XCTAssertFalse(accountManager.isUserAuthenticated) + + await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToSetSubscription) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { + // Given + ensureUserUnauthenticatedState() + + authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) + authService.validateTokenResult = .failure(Constants.invalidTokenError) + + let onSetSubscriptionCalled = expectation(description: "onSetSubscription") + onSetSubscriptionCalled.isInverted = true + feature.onSetSubscription = { + onSetSubscriptionCalled.fulfill() + } + + // When + let setSubscriptionParams = ["token": Constants.authToken] + let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + + // Then + XCTAssertNil(accountManager.authToken) + XCTAssertFalse(accountManager.isUserAuthenticated) + + await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToSetSubscription) + + await XCTAssertPrivacyPixelsFired([]) + } + + // MARK: - Tests for activateSubscription + + func testActivateSubscriptionTokenSuccess() async throws { + // Given + ensureUserAuthenticatedState() + + let onActivateSubscriptionCalled = expectation(description: "onActivateSubscription") + feature.onActivateSubscription = { + onActivateSubscriptionCalled.fulfill() + } + + // When + let result = await feature.activateSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + await fulfillment(of: [onActivateSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProRestorePurchaseOfferPageEntry.name]) + } + + // MARK: - Tests for featureSelected + + func testFeatureSelectedSuccess() async throws { + // Given + ensureUserAuthenticatedState() + + let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") + feature.onFeatureSelected = { selection in + onFeatureSelectedCalled.fulfill() + XCTAssertEqual(selection, SubscriptionFeatureSelection.itr) + } + + // When + let featureSelectionParams = ["feature": SubscriptionFeatureName.itr] + let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) + + // Then + await fulfillment(of: [onFeatureSelectedCalled], timeout: 0.5) + XCTAssertNil(result) + + await XCTAssertPrivacyPixelsFired([]) + } + + // MARK: - Tests for backToSettings + + func testBackToSettingsSuccess() async throws { + // Given + ensureUserAuthenticatedState() + accountStorage.email = nil + + XCTAssertNil(accountManager.email) + + let onBackToSettingsCalled = expectation(description: "onBackToSettings") + feature.onBackToSettings = { + onBackToSettingsCalled.fulfill() + } + + authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + + // When + let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) + + XCTAssertEqual(accountManager.email, Constants.email) + XCTAssertNil(result) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testBackToSettingsErrorOnFetchingAccountDetails() async throws { + // Given + ensureUserAuthenticatedState() + + let onBackToSettingsCalled = expectation(description: "onBackToSettings") + onBackToSettingsCalled.isInverted = true + feature.onBackToSettings = { + onBackToSettingsCalled.fulfill() + } + + authService.validateTokenResult = .failure(Constants.invalidTokenError) + + // When + let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) + + XCTAssertEqual(feature.transactionError, .generalError) + XCTAssertNil(result) + + await XCTAssertPrivacyPixelsFired([]) + } + + // MARK: - Tests for getAccessToken + func testGetAccessTokenSuccess() async throws { + // Given + ensureUserAuthenticatedState() + + // When + let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.accessToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testGetAccessTokenEmptyOnMissingToken() async throws { + // Given + ensureUserUnauthenticatedState() + XCTAssertNil(accountManager.accessToken) + + // When + let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + XCTAssertEqual(resultDictionary, [String: String]()) + + await XCTAssertPrivacyPixelsFired([]) + } + + // MARK: - Tests for restoreAccountFromAppStorePurchase + + func testRestoreAccountFromAppStorePurchaseSuccess() async throws { + // Given + ensureUserUnauthenticatedState() + + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS + authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, status: "authenticated")) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + + // When + try await feature.restoreAccountFromAppStorePurchase() + + // Then + XCTAssertTrue(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + + func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { + // Given + ensureUserUnauthenticatedState() + + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS + authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, status: "authenticated")) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + + + do { + // When + try await feature.restoreAccountFromAppStorePurchase() + XCTFail("Unexpected success") + } catch let error { + // Then + guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { + XCTFail("Unexpected error type") + return + } + + XCTAssertEqual(error, .subscriptionExpired) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + } + + func testRestoreAccountFromAppStorePurchaseErrorDueToNoTransaction() async throws { + // Given + ensureUserUnauthenticatedState() + + storePurchaseManager.mostRecentTransactionResult = nil + + do { + // When + try await feature.restoreAccountFromAppStorePurchase() + XCTFail("Unexpected success") + } catch let error { + // Then + guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { + XCTFail("Unexpected error type") + return + } + + XCTAssertEqual(error, .subscriptionNotFound) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + } + + func testRestoreAccountFromAppStorePurchaseErrorDueToOtherError() async throws { + // Given + ensureUserUnauthenticatedState() + + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS + authService.storeLoginResult = .failure(Constants.invalidTokenError) + + do { + // When + try await feature.restoreAccountFromAppStorePurchase() + XCTFail("Unexpected success") + } catch let error { + // Then + guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { + XCTFail("Unexpected error type") + return + } + + XCTAssertEqual(error, .failedToRestorePastPurchase) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + + await XCTAssertPrivacyPixelsFired([]) + } + } +} + +extension SubscriptionPagesUseSubscriptionFeatureTests { + + func ensureUserAuthenticatedState() { + accountStorage.authToken = Constants.authToken + accountStorage.email = Constants.email + accountStorage.externalID = Constants.externalID + accessTokenStorage.accessToken = Constants.accessToken + } + + func ensureUserUnauthenticatedState() { + try? accessTokenStorage.removeAccessToken() + try? accountStorage.clearAuthenticationState() + } + + public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) async { + try? await Task.sleep(seconds: 0.1) + + let pixelsFired = Set(pixelsFired) + let expectedPixels = Set(pixels) + + // Assert expected pixels were fired + XCTAssertTrue(expectedPixels.isSubset(of: pixelsFired), + "Expected Privacy Pro pixels were not fired: \(expectedPixels.subtracting(pixelsFired))", + file: file, + line: line) + + // Assert no other Privacy Pro pixels were fired except the expected + let privacyProPixelPrefix = "m_privacy-pro" + let otherPixels = pixelsFired.subtracting(expectedPixels) + let otherPrivacyProPixels = otherPixels.filter { $0.hasPrefix(privacyProPixelPrefix) } + XCTAssertTrue(otherPrivacyProPixels.isEmpty, + "Unexpected Privacy Pro pixels fired: \(otherPrivacyProPixels)", + file: file, + line: line) } } From ec207d4e8293fe2d475acc27aa1089393f2d6f9d Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Tue, 17 Sep 2024 23:43:22 +0200 Subject: [PATCH 07/46] Bump BSK (#3360) Task/Issue URL: https://app.asana.com/0/1199230911884351/1208292211771741/f **Description**: - Bump BSK From c02237014841a96a9bdf8f0a7183db5a5bde4fb5 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 18 Sep 2024 00:43:10 +0200 Subject: [PATCH 08/46] Update build number --- 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 18e8d0eb00..e92e2d9265 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9100,7 +9100,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; @@ -9137,7 +9137,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; @@ -9227,7 +9227,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 = ( @@ -9254,7 +9254,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; @@ -9403,7 +9403,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; @@ -9428,7 +9428,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 = ( @@ -9497,7 +9497,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; @@ -9531,7 +9531,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; @@ -9564,7 +9564,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 = ( @@ -9594,7 +9594,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; @@ -9904,7 +9904,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; @@ -9935,7 +9935,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 = ( @@ -9963,7 +9963,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 = ( @@ -9996,7 +9996,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; @@ -10026,7 +10026,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; @@ -10059,11 +10059,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"; @@ -10296,7 +10296,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; @@ -10323,7 +10323,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; @@ -10355,7 +10355,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; @@ -10392,7 +10392,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; @@ -10427,7 +10427,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; @@ -10462,11 +10462,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"; @@ -10639,11 +10639,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"; @@ -10672,10 +10672,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 741daa5e803d7a5e90f4745017e81a60bfa22fd4 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 18 Sep 2024 00:45:22 +0200 Subject: [PATCH 09/46] [DuckPlayer] Experiment Fix - Update Test Variables and pixel names (#3363) Task/Issue URL: https://app.asana.com/0/1204099484721401/1208336598845809/f Description: Moves DuckPlayer launch experiment to be behind the Launch Feature flag to avoid enrolling users before time. (As it happened) Renames variables and pixels to avoid collisions with the old (bogus) experiment --- Core/PixelEvent.swift | 10 +-- .../DuckPlayerLaunchExperiment.swift | 66 +++++++++---------- .../DuckPlayerNavigationHandler.swift | 43 ++++++------ .../DuckPlayerExperimentTests.swift | 8 +-- 4 files changed, 66 insertions(+), 61 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2afe5960b8..c6e89c030f 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -1619,11 +1619,11 @@ extension Pixel.Event { case .pproFeedbackSubmitScreenFAQClick: return "m_ppro_feedback_submit-screen-faq_click" // MARK: Duckplayer experiment - case .duckplayerExperimentCohortAssign: return "duckplayer_experiment_cohort_assign" - case .duckplayerExperimentSearch: return "duckplayer_experiment_search" - case .duckplayerExperimentDailySearch: return "duckplayer_experiment_daily_search" - case .duckplayerExperimentWeeklySearch: return "duckplayer_experiment_weekly_search" - case .duckplayerExperimentYoutubePageView: return "duckplayer_experiment_youtube_page_view" + case .duckplayerExperimentCohortAssign: return "duckplayer_experiment_cohort_assign_v2" + case .duckplayerExperimentSearch: return "duckplayer_experiment_search_v2" + case .duckplayerExperimentDailySearch: return "duckplayer_experiment_daily_search_v2" + case .duckplayerExperimentWeeklySearch: return "duckplayer_experiment_weekly_search_v2" + case .duckplayerExperimentYoutubePageView: return "duckplayer_experiment_youtube_page_view_v2" } } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift index 19decaf548..8edd25ed56 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift @@ -77,19 +77,19 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { private let dateProvider: DuckPlayerExperimentDateProvider @UserDefaultsWrapper(key: .duckPlayerPixelExperimentLastWeekPixelFired, defaultValue: nil) - private var lastWeekPixelFired: Int? + private var lastWeekPixelFiredV2: Int? @UserDefaultsWrapper(key: .duckPlayerPixelExperimentLastDayPixelFired, defaultValue: nil) - private var lastDayPixelFired: Int? + private var lastDayPixelFiredV2: Int? @UserDefaultsWrapper(key: .duckPlayerPixelExperimentLastVideoIDRendered, defaultValue: nil) - private var lastVideoIDReported: String? + private var lastVideoIDReportedV2: String? @UserDefaultsWrapper(key: .duckPlayerPixelExperimentEnrollmentDate, defaultValue: nil) - var enrollmentDate: Date? + var enrollmentDateV2: Date? @UserDefaultsWrapper(key: .duckPlayerPixelExperimentCohort, defaultValue: nil) - var experimentCohort: String? + var experimentCohortV2: String? private var isInternalUser: Bool @@ -113,7 +113,7 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { private var dates: (day: Int, week: Int)? { guard isEnrolled, - let enrollmentDate = enrollmentDate else { return nil } + let enrollmentDate = enrollmentDateV2 else { return nil } let currentDate = dateProvider.currentDate let calendar = Calendar.current let dayDifference = calendar.dateComponents([.day], from: enrollmentDate, to: currentDate).day ?? 0 @@ -123,7 +123,7 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { private var formattedEnrollmentDate: String? { guard isEnrolled, - let enrollmentDate = enrollmentDate else { return nil } + let enrollmentDate = enrollmentDateV2 else { return nil } return Self.formattedDate(enrollmentDate) } @@ -135,11 +135,11 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { } var isEnrolled: Bool { - return enrollmentDate != nil && experimentCohort != nil + return enrollmentDateV2 != nil && experimentCohortV2 != nil } var isExperimentCohort: Bool { - return experimentCohort == "experiment" + return experimentCohortV2 == "experiment" } func assignUserToCohort() { @@ -149,32 +149,32 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { if isInternalUser { cohort = .experiment } - experimentCohort = cohort.rawValue - enrollmentDate = dateProvider.currentDate + experimentCohortV2 = cohort.rawValue + enrollmentDateV2 = dateProvider.currentDate fireEnrollmentPixel() } } private func fireEnrollmentPixel() { guard isEnrolled, - let experimentCohort = experimentCohort, + let experimentCohortV2 = experimentCohortV2, let formattedEnrollmentDate else { return } - let params = [Constants.variantKey: experimentCohort, Constants.enrollmentKey: formattedEnrollmentDate] + let params = [Constants.variantKey: experimentCohortV2, Constants.enrollmentKey: formattedEnrollmentDate] pixel.fireDuckPlayerExperimentPixel(pixel: .duckplayerExperimentCohortAssign, withAdditionalParameters: params) } func fireSearchPixels() { if isEnrolled { guard isEnrolled, - let experimentCohort = experimentCohort, + let experimentCohortV2 = experimentCohortV2, let dates, let formattedEnrollmentDate else { return } var params = [ - Constants.variantKey: experimentCohort, + Constants.variantKey: experimentCohortV2, Constants.dayKey: "\(dates.day)", Constants.enrollmentKey: formattedEnrollmentDate ] @@ -183,56 +183,56 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { pixel.fireDuckPlayerExperimentPixel(pixel: .duckplayerExperimentSearch, withAdditionalParameters: params) // Fire a daily pixel - if dates.day != lastDayPixelFired { + if dates.day != lastDayPixelFiredV2 { pixel.fireDuckPlayerExperimentPixel(pixel: .duckplayerExperimentDailySearch, withAdditionalParameters: params) - lastDayPixelFired = dates.day + lastDayPixelFiredV2 = dates.day } // Fire a weekly pixel - if dates.week != lastWeekPixelFired && dates.day > 0 { + if dates.week != lastWeekPixelFiredV2 && dates.day > 0 { params.removeValue(forKey: Constants.dayKey) params[Constants.weekKey] = "\(dates.week)" pixel.fireDuckPlayerExperimentPixel(pixel: .duckplayerExperimentWeeklySearch, withAdditionalParameters: params) - lastWeekPixelFired = dates.week + lastWeekPixelFiredV2 = dates.week } } } func fireYoutubePixel(videoID: String) { guard isEnrolled, - let experimentCohort = experimentCohort, + let experimentCohortV2 = experimentCohortV2, let dates, let formattedEnrollmentDate else { return } let params = [ - Constants.variantKey: experimentCohort, + Constants.variantKey: experimentCohortV2, Constants.dayKey: "\(dates.day)", Constants.stateKey: duckPlayerMode?.stringValue ?? "", Constants.referrerKey: referrer?.stringValue ?? "", Constants.enrollmentKey: formattedEnrollmentDate ] - if lastVideoIDReported != videoID { + if lastVideoIDReportedV2 != videoID { pixel.fireDuckPlayerExperimentPixel(pixel: .duckplayerExperimentYoutubePageView, withAdditionalParameters: params) - lastVideoIDReported = videoID + lastVideoIDReportedV2 = videoID } } func cleanup() { - enrollmentDate = nil - experimentCohort = nil - lastDayPixelFired = nil - lastWeekPixelFired = nil - lastVideoIDReported = nil + enrollmentDateV2 = nil + experimentCohortV2 = nil + lastDayPixelFiredV2 = nil + lastWeekPixelFiredV2 = nil + lastVideoIDReportedV2 = nil } func override() { - enrollmentDate = Date() - experimentCohort = "experiment" - lastDayPixelFired = nil - lastWeekPixelFired = nil - lastVideoIDReported = nil + enrollmentDateV2 = Date() + experimentCohortV2 = "experiment" + lastDayPixelFiredV2 = nil + lastWeekPixelFiredV2 = nil + lastVideoIDReportedV2 = nil } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift index 96eb8ec30f..b2316a331e 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift @@ -34,6 +34,7 @@ final class DuckPlayerNavigationHandler { var featureFlagger: FeatureFlagger var appSettings: AppSettings var experiment: DuckPlayerLaunchExperimentHandling + private lazy var internalUserDecider = AppDependencyProvider.shared.internalUserDecider private struct Constants { static let SERPURL = "duckduckgo.com/" @@ -216,26 +217,30 @@ final class DuckPlayerNavigationHandler { if let navigationAction, isSERPLink(navigationAction: navigationAction) { referrer = .serp } - - - // DuckPlayer Experiment run - let experiment = DuckPlayerLaunchExperiment(duckPlayerMode: duckPlayerMode, referrer: referrer) - - // Enroll user if not enrolled - if !experiment.isEnrolled { - experiment.assignUserToCohort() - } - - // DuckPlayer is disabled before user enrolls, - // So trigger a settings change notification - // to let the FE know about the 'actual' setting - // and update Experiment value - if experiment.isExperimentCohort { - duckPlayer.settings.triggerNotification() - experiment.duckPlayerMode = duckPlayer.settings.mode + + if featureFlagger.isFeatureOn(.duckPlayer) || internalUserDecider.isInternalUser { + + // DuckPlayer Experiment run + let experiment = DuckPlayerLaunchExperiment(duckPlayerMode: duckPlayerMode, + referrer: referrer, + isInternalUser: internalUserDecider.isInternalUser) + + // Enroll user if not enrolled + if !experiment.isEnrolled { + experiment.assignUserToCohort() + } + + // DuckPlayer is disabled before user enrolls, + // So trigger a settings change notification + // to let the FE know about the 'actual' setting + // and update Experiment value + if experiment.isExperimentCohort { + duckPlayer.settings.triggerNotification() + experiment.duckPlayerMode = duckPlayer.settings.mode + } + + experiment.fireYoutubePixel(videoID: videoID) } - - experiment.fireYoutubePixel(videoID: videoID) } diff --git a/DuckDuckGoTests/DuckPlayerExperimentTests.swift b/DuckDuckGoTests/DuckPlayerExperimentTests.swift index 86b8f17817..56040b0dee 100644 --- a/DuckDuckGoTests/DuckPlayerExperimentTests.swift +++ b/DuckDuckGoTests/DuckPlayerExperimentTests.swift @@ -106,9 +106,9 @@ final class DuckPlayerLaunchExperimentTests: XCTestCase { sut.assignUserToCohort() XCTAssertTrue(sut.isEnrolled, "User should be enrolled after assigning to cohort.") - XCTAssertNotNil(sut.experimentCohort, "Experiment cohort should be assigned.") - XCTAssertNotNil(sut.enrollmentDate, "Enrollment date should be set.") - XCTAssertEqual(DuckPlayerLaunchExperiment.formattedDate(sut.enrollmentDate ?? Date()), "20240910", "The assigned date should match.") + XCTAssertNotNil(sut.experimentCohortV2, "Experiment cohort should be assigned.") + XCTAssertNotNil(sut.enrollmentDateV2, "Enrollment date should be set.") + XCTAssertEqual(DuckPlayerLaunchExperiment.formattedDate(sut.enrollmentDateV2 ?? Date()), "20240910", "The assigned date should match.") // Check the pixel event history let history = DuckPlayerExperimentPixelFireMock.capturedPixelEventHistory @@ -142,7 +142,7 @@ final class DuckPlayerLaunchExperimentTests: XCTestCase { sut.assignUserToCohort() XCTAssertEqual(DuckPlayerExperimentPixelFireMock.capturedPixelEventHistory.count, 0, "Enrollment pixel should not have fired again") XCTAssertEqual(sut.isEnrolled, true, "The assigned date should not change.") - XCTAssertEqual(DuckPlayerLaunchExperiment.formattedDate(sut.enrollmentDate ?? Date()), "20240910", "The assigned date should not change.") + XCTAssertEqual(DuckPlayerLaunchExperiment.formattedDate(sut.enrollmentDateV2 ?? Date()), "20240910", "The assigned date should not change.") } func testIfUserIsEnrolled_SearchDailyPixelsFire() { From 0da64c103f8a9120fbfa38468ebe2e0f11035c4c Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 18 Sep 2024 00:59:09 +0200 Subject: [PATCH 10/46] Update Variable names --- Core/UserDefaultsPropertyWrapper.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 11fcd46d61..6df337d168 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -172,13 +172,13 @@ public struct UserDefaultsWrapper { case debugOnboardingHighlightsEnabledKey = "com.duckduckgo.ios.debug.onboardingHighlightsEnabled" // Duck Player Pixel Experiment - case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed" - case duckPlayerPixelExperimentCohort = "com.duckduckgo.ios.duckplayer.pixel.experiment.cohort" - case duckPlayerPixelExperimentEnrollmentDate = "com.duckduckgo.ios.duckplayer.pixel.experiment.enrollment.date" - case duckPlayerPixelExperimentLastWeekPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.week.pixel.fired" - case duckPlayerPixelExperimentLastDayPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.day.pixel.fired" - case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered" - case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override" + case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed.v2" + case duckPlayerPixelExperimentCohort = "com.duckduckgo.ios.duckplayer.pixel.experiment.cohort.v2" + case duckPlayerPixelExperimentEnrollmentDate = "com.duckduckgo.ios.duckplayer.pixel.experiment.enrollment.date.v2" + case duckPlayerPixelExperimentLastWeekPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.week.pixel.fired.v2" + case duckPlayerPixelExperimentLastDayPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.day.pixel.fired.v2" + case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered.v2" + case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override.v2" } From e8704876555724f92df224a8872c0397827f2946 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 18 Sep 2024 01:09:12 +0200 Subject: [PATCH 11/46] Release 7.138.0-1 (#3364) --- Core/UserDefaultsPropertyWrapper.swift | 14 +++---- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++++++++++------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 11fcd46d61..6df337d168 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -172,13 +172,13 @@ public struct UserDefaultsWrapper { case debugOnboardingHighlightsEnabledKey = "com.duckduckgo.ios.debug.onboardingHighlightsEnabled" // Duck Player Pixel Experiment - case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed" - case duckPlayerPixelExperimentCohort = "com.duckduckgo.ios.duckplayer.pixel.experiment.cohort" - case duckPlayerPixelExperimentEnrollmentDate = "com.duckduckgo.ios.duckplayer.pixel.experiment.enrollment.date" - case duckPlayerPixelExperimentLastWeekPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.week.pixel.fired" - case duckPlayerPixelExperimentLastDayPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.day.pixel.fired" - case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered" - case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override" + case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed.v2" + case duckPlayerPixelExperimentCohort = "com.duckduckgo.ios.duckplayer.pixel.experiment.cohort.v2" + case duckPlayerPixelExperimentEnrollmentDate = "com.duckduckgo.ios.duckplayer.pixel.experiment.enrollment.date.v2" + case duckPlayerPixelExperimentLastWeekPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.week.pixel.fired.v2" + case duckPlayerPixelExperimentLastDayPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.day.pixel.fired.v2" + case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered.v2" + case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override.v2" } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 18e8d0eb00..e92e2d9265 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9100,7 +9100,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; @@ -9137,7 +9137,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; @@ -9227,7 +9227,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 = ( @@ -9254,7 +9254,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; @@ -9403,7 +9403,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; @@ -9428,7 +9428,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 = ( @@ -9497,7 +9497,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; @@ -9531,7 +9531,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; @@ -9564,7 +9564,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 = ( @@ -9594,7 +9594,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; @@ -9904,7 +9904,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; @@ -9935,7 +9935,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 = ( @@ -9963,7 +9963,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 = ( @@ -9996,7 +9996,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; @@ -10026,7 +10026,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; @@ -10059,11 +10059,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"; @@ -10296,7 +10296,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; @@ -10323,7 +10323,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; @@ -10355,7 +10355,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; @@ -10392,7 +10392,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; @@ -10427,7 +10427,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; @@ -10462,11 +10462,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"; @@ -10639,11 +10639,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"; @@ -10672,10 +10672,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 05718f9db33cb9731219d8b0de233a6167978219 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 18 Sep 2024 01:52:44 +0200 Subject: [PATCH 12/46] Bump version number --- 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 e92e2d9265..4fc50dc02a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9100,7 +9100,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9137,7 +9137,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9227,7 +9227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9254,7 +9254,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9403,7 +9403,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9428,7 +9428,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9497,7 +9497,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9531,7 +9531,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9564,7 +9564,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9594,7 +9594,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9904,7 +9904,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9935,7 +9935,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9963,7 +9963,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9996,7 +9996,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10026,7 +10026,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10059,11 +10059,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10296,7 +10296,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10323,7 +10323,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10355,7 +10355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10392,7 +10392,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10427,7 +10427,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10462,11 +10462,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10639,11 +10639,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10672,10 +10672,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 56cc6c7fec4480f9201cca732ab218514a70a0c6 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 18 Sep 2024 13:26:13 +0200 Subject: [PATCH 13/46] Fix Network Controller leak and Dependency Provider usage (#3367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414235014887631/1208331423616717/f Tech Design URL: CC: **Description**: Fix Network Controller leak and Dependency Provider usage so it doesn't generate multiple instances unnecessarily. **Steps to test this PR**: Run app with allocations & leaks Instruments to ensure no extra instances of Network related classes are created. Smoke test app instantiation and lifecycle around DI changes. **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) --------- Co-authored-by: Chris Brind --- DuckDuckGo/NetworkProtectionTunnelController.swift | 7 +++++-- DuckDuckGo/StubAutofillLoginImportStateProvider.swift | 4 ++-- DuckDuckGo/TabManager.swift | 4 ++-- DuckDuckGo/TabViewControllerLongPressMenuExtension.swift | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index fe6e1620ee..c7b8ebee6d 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -322,7 +322,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private func subscribeToConfigurationChanges() { notificationCenter.publisher(for: .NEVPNConfigurationChange) .receive(on: DispatchQueue.main) - .sink { _ in + .sink { [weak self] _ in + guard let self = self else { return } Task { @MainActor in guard let manager = self.internalManager else { return @@ -346,7 +347,9 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private func subscribeToStatusChanges() { notificationCenter.publisher(for: .NEVPNStatusDidChange) - .sink(receiveValue: handleStatusChange(_:)) + .sink { [weak self] value in + self?.handleStatusChange(value) + } .store(in: &cancellables) } diff --git a/DuckDuckGo/StubAutofillLoginImportStateProvider.swift b/DuckDuckGo/StubAutofillLoginImportStateProvider.swift index 326659f898..2b9c6382ab 100644 --- a/DuckDuckGo/StubAutofillLoginImportStateProvider.swift +++ b/DuckDuckGo/StubAutofillLoginImportStateProvider.swift @@ -26,10 +26,10 @@ struct StubAutofillLoginImportStateProvider: AutofillLoginImportStateProvider { var credentialsImportPromptPresentationCount: Int = 0 var isAutofillEnabled: Bool { - AppDependencyProvider().appSettings.autofillCredentialsEnabled + AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled } func hasNeverPromptWebsitesFor(_ domain: String) -> Bool { - AppDependencyProvider().autofillNeverPromptWebsitesManager.hasNeverPromptWebsitesFor(domain: domain) + AppDependencyProvider.shared.autofillNeverPromptWebsitesManager.hasNeverPromptWebsitesFor(domain: domain) } } diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index 1d7fadf8ca..24077c9454 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -89,7 +89,7 @@ class TabManager { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider().featureFlagger) + featureFlagger: AppDependencyProvider.shared.featureFlagger) controller.applyInheritedAttribution(inheritedAttribution) controller.attachWebView(configuration: configuration, andLoadRequest: url == nil ? nil : URLRequest.userInitiated(url!), @@ -167,7 +167,7 @@ class TabManager { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider().featureFlagger) + featureFlagger: AppDependencyProvider.shared.featureFlagger) controller.attachWebView(configuration: configCopy, andLoadRequest: request, consumeCookies: !model.hasActiveTabs, diff --git a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index 938abdacd5..38770be4e7 100644 --- a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -111,7 +111,7 @@ extension TabViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider().featureFlagger) + featureFlagger: AppDependencyProvider.shared.featureFlagger) tabController.isLinkPreview = true let configuration = WKWebViewConfiguration.nonPersistent() tabController.attachWebView(configuration: configuration, andLoadRequest: URLRequest.userInitiated(url), consumeCookies: false) From 691d8af6700b2e0ebe05b1402321db1b1ccffe2a Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 18 Sep 2024 13:42:00 +0200 Subject: [PATCH 14/46] Release 7.138.0-3 (#3368) 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 4fc50dc02a..2686c74017 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9100,7 +9100,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9137,7 +9137,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9227,7 +9227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9254,7 +9254,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9403,7 +9403,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9428,7 +9428,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9497,7 +9497,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9531,7 +9531,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9564,7 +9564,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9594,7 +9594,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9904,7 +9904,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9935,7 +9935,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9963,7 +9963,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9996,7 +9996,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10026,7 +10026,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10059,11 +10059,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10296,7 +10296,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10323,7 +10323,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10355,7 +10355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10392,7 +10392,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10427,7 +10427,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10462,11 +10462,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10639,11 +10639,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10672,10 +10672,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 7b3cca106525d4e2a10162fcbe54609aad57877f Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 19 Sep 2024 19:02:10 +1000 Subject: [PATCH 15/46] Onboarding highlights pixels (#3365) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208084960727012 **Description**: 1. Add pixels to Onboarding Highlights experiment. [Commit](https://github.com/duckduckgo/iOS/commit/e57c3fbd477fc3252d822fb165bd96da0062e9c7) 2. Remove previous onboarding experiment temporary pixels. [Commit](https://github.com/duckduckgo/iOS/commit/ccc4a0117f2c18608ff1e2ee2ba8afe99dca8734) --- Core/PixelEvent.swift | 12 ++ .../NewTabDaxDialogFactory.swift | 3 +- .../ContextualDaxDialogsFactory.swift | 3 +- .../OnboardingIntroViewModel.swift | 18 +- .../Pixels/OnboardingPixelReporter.swift | 35 +++- .../ContextualDaxDialogsFactoryTests.swift | 14 ++ ...alOnboardingNewTabDialogFactoryTests.swift | 21 ++- .../OnboardingIntroViewModelTests.swift | 156 +++++++++++++----- .../OnboardingPixelReporterMock.swift | 31 +++- .../OnboardingPixelReporterTests.swift | 130 ++++++++++----- 10 files changed, 324 insertions(+), 99 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index b9a2cf6dae..2f67e300f1 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -144,6 +144,11 @@ extension Pixel { case onboardingIntroShownUnique case onboardingIntroComparisonChartShownUnique case onboardingIntroChooseBrowserCTAPressed + case onboardingIntroChooseAppIconImpressionUnique + case onboardingIntroChooseCustomAppIconColorCTAPressed + case onboardingIntroChooseAddressBarImpressionUnique + case onboardingIntroBottomAddressBarSelected + case onboardingContextualSearchOptionTappedUnique case onboardingContextualSearchCustomUnique case onboardingContextualSiteOptionTappedUnique @@ -164,6 +169,7 @@ extension Pixel { case daxDialogsFireEducationCancelledUnique case daxDialogsEndOfJourneyTabUnique case daxDialogsEndOfJourneyNewTabUnique + case daxDialogsEndOfJourneyDismissed case widgetsOnboardingCTAPressed case widgetsOnboardingDeclineOptionPressed @@ -953,6 +959,11 @@ extension Pixel.Event { case .onboardingIntroShownUnique: return "m_preonboarding_intro_shown_unique" case .onboardingIntroComparisonChartShownUnique: return "m_preonboarding_comparison_chart_shown_unique" case .onboardingIntroChooseBrowserCTAPressed: return "m_preonboarding_choose_browser_pressed" + case .onboardingIntroChooseAppIconImpressionUnique: return "m_preonboarding_choose_icon_impressions_unique" + case .onboardingIntroChooseCustomAppIconColorCTAPressed: return "m_preonboarding_icon_color_chosen" + case .onboardingIntroChooseAddressBarImpressionUnique: return "m_preonboarding_choose_address_bar_impressions_unique" + case .onboardingIntroBottomAddressBarSelected: return "m_preonboarding_bottom_address_bar_selected" + case .onboardingContextualSearchOptionTappedUnique: return "m_onboarding_search_option_tapped_unique" case .onboardingContextualSiteOptionTappedUnique: return "m_onboarding_visit_site_option_tapped_unique" case .onboardingContextualSecondSiteVisitUnique: return "m_second_sitevisit_unique" @@ -973,6 +984,7 @@ extension Pixel.Event { case .daxDialogsFireEducationCancelledUnique: return "m_dx_fe_ca_unique" case .daxDialogsEndOfJourneyTabUnique: return "m_dx_end_tab_unique" case .daxDialogsEndOfJourneyNewTabUnique: return "m_dx_end_new_tab_unique" + case .daxDialogsEndOfJourneyDismissed: return "m_dx_end_dialog_dismissed" case .widgetsOnboardingCTAPressed: return "m_o_w_a" case .widgetsOnboardingDeclineOptionPressed: return "m_o_w_d" diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index 7f6f068844..6d99fc21d9 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -102,7 +102,8 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage return FadeInView { - OnboardingFinalDialog(message: message, highFiveAction: { + OnboardingFinalDialog(message: message, highFiveAction: { [weak self] in + self?.onboardingPixelReporter.trackEndOfJourneyDialogCTAAction() onDismiss() }) .onboardingDaxDialogStyle() diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index 04fa68f5df..2ca605f244 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -175,8 +175,9 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { private func endOfJourneyDialog(delegate: ContextualOnboardingDelegate, pixelName: Pixel.Event) -> some View { let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage - return OnboardingFinalDialog(message: message, highFiveAction: { [weak delegate] in + return OnboardingFinalDialog(message: message, highFiveAction: { [weak delegate, weak self] in delegate?.didTapDismissContextualOnboardingAction() + self?.contextualOnboardingPixelReporter.trackEndOfJourneyDialogCTAAction() }) .onFirstAppear { [weak self] in self?.contextualOnboardingLogic.setFinalOnboardingDialogSeen() diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index 419ea7daf1..af1622a3d0 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -34,17 +34,24 @@ final class OnboardingIntroViewModel: ObservableObject { private let onboardingManager: OnboardingHighlightsManaging private let isIpad: Bool private let urlOpener: URLOpener + private let appIconProvider: () -> AppIcon + private let addressBarPositionProvider: () -> AddressBarPosition init( pixelReporter: OnboardingIntroPixelReporting, onboardingManager: OnboardingHighlightsManaging = OnboardingManager(), isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, - urlOpener: URLOpener = UIApplication.shared + urlOpener: URLOpener = UIApplication.shared, + appIconProvider: @escaping () -> AppIcon = { AppIconManager.shared.appIcon }, + addressBarPositionProvider: @escaping () -> AddressBarPosition = { AppUserDefaults().currentAddressBarPosition } ) { self.pixelReporter = pixelReporter self.onboardingManager = onboardingManager self.isIpad = isIpad self.urlOpener = urlOpener + self.appIconProvider = appIconProvider + self.addressBarPositionProvider = addressBarPositionProvider + introSteps = if onboardingManager.isOnboardingHighlightsEnabled { isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsIPhoneFlow } else { @@ -79,14 +86,22 @@ final class OnboardingIntroViewModel: ObservableObject { } func appIconPickerContinueAction() { + if appIconProvider() != .defaultAppIcon { + pixelReporter.trackChooseCustomAppIconColor() + } + if isIpad { onCompletingOnboardingIntro?() } else { state = makeViewState(for: .addressBarPositionSelection) + pixelReporter.trackAddressBarPositionSelectionImpression() } } func selectAddressBarPositionAction() { + if addressBarPositionProvider() == .bottom { + pixelReporter.trackChooseBottomAddressBarPosition() + } onCompletingOnboardingIntro?() } @@ -127,6 +142,7 @@ private extension OnboardingIntroViewModel { func handleSetDefaultBrowserAction() { if onboardingManager.isOnboardingHighlightsEnabled { state = makeViewState(for: .appIconSelection) + pixelReporter.trackChooseAppIconImpression() } else { onCompletingOnboardingIntro?() } diff --git a/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift b/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift index ce0cfa348d..1882e2c3e7 100644 --- a/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift +++ b/DuckDuckGo/OnboardingExperiment/Pixels/OnboardingPixelReporter.swift @@ -49,6 +49,10 @@ protocol OnboardingIntroImpressionReporting { protocol OnboardingIntroPixelReporting: OnboardingIntroImpressionReporting { func trackBrowserComparisonImpression() func trackChooseBrowserCTAAction() + func trackChooseAppIconImpression() + func trackChooseCustomAppIconColor() + func trackAddressBarPositionSelectionImpression() + func trackChooseBottomAddressBarPosition() } protocol OnboardingCustomInteractionPixelReporting { @@ -58,11 +62,12 @@ protocol OnboardingCustomInteractionPixelReporting { func trackPrivacyDashboardOpenedForFirstTime() } -protocol OnboardingScreenImpressionReporting { +protocol OnboardingDaxDialogsReporting { func trackScreenImpression(event: Pixel.Event) + func trackEndOfJourneyDialogCTAAction() } -typealias OnboardingPixelReporting = OnboardingIntroImpressionReporting & OnboardingIntroPixelReporting & OnboardingSearchSuggestionsPixelReporting & OnboardingSiteSuggestionsPixelReporting & OnboardingCustomInteractionPixelReporting & OnboardingScreenImpressionReporting +typealias OnboardingPixelReporting = OnboardingIntroImpressionReporting & OnboardingIntroPixelReporting & OnboardingSearchSuggestionsPixelReporting & OnboardingSiteSuggestionsPixelReporting & OnboardingCustomInteractionPixelReporting & OnboardingDaxDialogsReporting // MARK: - Implementation @@ -146,6 +151,22 @@ extension OnboardingPixelReporter: OnboardingIntroPixelReporting { fire(event: .onboardingIntroChooseBrowserCTAPressed, unique: false) } + func trackChooseAppIconImpression() { + fire(event: .onboardingIntroChooseAppIconImpressionUnique, unique: true, includedParameters: [.appVersion]) + } + + func trackChooseCustomAppIconColor() { + fire(event: .onboardingIntroChooseCustomAppIconColorCTAPressed, unique: false, includedParameters: [.appVersion]) + } + + func trackAddressBarPositionSelectionImpression() { + fire(event: .onboardingIntroChooseAddressBarImpressionUnique, unique: true, includedParameters: [.appVersion]) + } + + func trackChooseBottomAddressBarPosition() { + fire(event: .onboardingIntroBottomAddressBarSelected, unique: false, includedParameters: [.appVersion]) + } + } // MARK: - OnboardingPixelReporter + List @@ -153,7 +174,7 @@ extension OnboardingPixelReporter: OnboardingIntroPixelReporting { extension OnboardingPixelReporter: OnboardingSearchSuggestionsPixelReporting { func trackSearchSuggetionOptionTapped() { - fire(event: .onboardingContextualSearchOptionTappedUnique, unique: true) + // Left empty on purpose. These were temporary pixels in iOS. macOS will still use them. } } @@ -161,7 +182,7 @@ extension OnboardingPixelReporter: OnboardingSearchSuggestionsPixelReporting { extension OnboardingPixelReporter: OnboardingSiteSuggestionsPixelReporting { func trackSiteSuggetionOptionTapped() { - fire(event: .onboardingContextualSiteOptionTappedUnique, unique: true) + // Left empty on purpose. These were temporary pixels in iOS. macOS will still use them. } } @@ -199,12 +220,16 @@ extension OnboardingPixelReporter: OnboardingCustomInteractionPixelReporting { // MARK: - OnboardingPixelReporter + Screen Impression -extension OnboardingPixelReporter: OnboardingScreenImpressionReporting { +extension OnboardingPixelReporter: OnboardingDaxDialogsReporting { func trackScreenImpression(event: Pixel.Event) { fire(event: event, unique: true) } + func trackEndOfJourneyDialogCTAAction() { + fire(event: .daxDialogsEndOfJourneyDismissed, unique: false) + } + } struct EnqueuedPixel { diff --git a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift index edafab97ab..cf4b1ba610 100644 --- a/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift +++ b/DuckDuckGoTests/ContextualDaxDialogsFactoryTests.swift @@ -321,6 +321,20 @@ final class ContextualDaxDialogsFactoryTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackScreenImpressionCalled) XCTAssertEqual(pixelReporterMock.capturedScreenImpression, .onboardingContextualTryVisitSiteUnique) } + + func testWhenEndOfJourneyDialogCTAIsTappedThenExpectedPixelFires() throws { + // GIVEN + let spec = DaxDialogs.BrowsingSpec.final + let result = sut.makeView(for: spec, delegate: delegate, onSizeUpdate: {}) + let view = try XCTUnwrap(find(OnboardingFinalDialog.self, in: result)) + XCTAssertFalse(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) + + // WHEN + view.highFiveAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) + } } extension ContextualDaxDialogsFactoryTests { diff --git a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift index 5e1f62a30a..152b5523e4 100644 --- a/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift +++ b/DuckDuckGoTests/ContextualOnboardingNewTabDialogFactoryTests.swift @@ -124,7 +124,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { // MARK: - Pixels - func testWhenOnboardingTrySearchDialogAppearForTheFirstTime_ThenSendFireExpectedPixel() { + func testWhenOnboardingTrySearchDialogAppearForTheFirstTime_ThenFireExpectedPixel() { // GIVEN let spec = DaxDialogs.HomeScreenSpec.initial let pixelEvent = Pixel.Event.onboardingContextualTrySearchUnique @@ -132,7 +132,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { testDialogDefinedBy(spec: spec, firesEvent: pixelEvent) } - func testWhenOnboardingTryVisitSiteDialogAppearForTheFirstTime_ThenSendFireExpectedPixel() { + func testWhenOnboardingTryVisitSiteDialogAppearForTheFirstTime_ThenFireExpectedPixel() { // GIVEN let spec = DaxDialogs.HomeScreenSpec.subsequent let pixelEvent = Pixel.Event.onboardingContextualTryVisitSiteUnique @@ -140,7 +140,7 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { testDialogDefinedBy(spec: spec, firesEvent: pixelEvent) } - func testWhenOnboardingFinalDialogAppearForTheFirstTime_ThenSendFireExpectedPixel() { + func testWhenOnboardingFinalDialogAppearForTheFirstTime_ThenFireExpectedPixel() { // GIVEN let spec = DaxDialogs.HomeScreenSpec.final let pixelEvent = Pixel.Event.daxDialogsEndOfJourneyNewTabUnique @@ -148,6 +148,21 @@ class ContextualOnboardingNewTabDialogFactoryTests: XCTestCase { testDialogDefinedBy(spec: spec, firesEvent: pixelEvent) } + func testWhenOnboardingFinalDialogCTAIsTapped_ThenFireExpectedPixel() throws { + // GIVEN + let view = factory.createDaxDialog(for: DaxDialogs.HomeScreenSpec.final, onDismiss: {}) + let host = UIHostingController(rootView: view) + window.rootViewController = host + let finalDialog = try XCTUnwrap(find(OnboardingFinalDialog.self, in: host)) + XCTAssertFalse(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) + + // WHEN + finalDialog.highFiveAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackEndOfJourneyDialogDismiss) + } + } private extension ContextualOnboardingNewTabDialogFactoryTests { diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index 520ded80cf..2c8ee42d50 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -37,7 +37,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSubscribeToViewStateThenShouldSendLanding() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.state @@ -48,7 +48,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenOnAppearIsCalledThenViewStateChangesToStartOnboardingDialog() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -60,7 +60,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStartOnboardingActionIsCalledThenViewStateChangesToBrowsersComparisonDialog() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager) XCTAssertEqual(sut.state, .landing) // WHEN @@ -73,7 +73,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSetDefaultBrowserActionIsCalledThenURLOpenerOpensSettingsURL() { // GIVEN let urlOpenerMock = MockURLOpener() - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: urlOpenerMock) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: urlOpenerMock) XCTAssertFalse(urlOpenerMock.didCallOpenURL) XCTAssertNil(urlOpenerMock.capturedURL) @@ -88,7 +88,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -104,7 +104,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -124,7 +124,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSubscribeToViewStateAndIsHighlightsIphoneFlowThenShouldSendLanding() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) // WHEN let result = sut.state @@ -136,7 +136,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenOnAppearIsCalledAndAndIsHighlightsIphoneFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -149,7 +149,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStartOnboardingActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) XCTAssertEqual(sut.state, .landing) // WHEN @@ -162,7 +162,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -175,7 +175,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -188,7 +188,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) XCTAssertEqual(sut.state, .landing) // WHEN @@ -202,7 +202,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -220,7 +220,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSubscribeToViewStateAndIsHighlightsIpadFlowThenShouldSendLanding() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) // WHEN let result = sut.state @@ -232,7 +232,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenOnAppearIsCalledAndAndIsHighlightsIpadFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -245,7 +245,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStartOnboardingActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true) XCTAssertEqual(sut.state, .landing) // WHEN @@ -258,7 +258,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -271,7 +271,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN @@ -285,7 +285,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -302,7 +302,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenOnAppearIsCalledThenPixelReporterTrackOnboardingIntroImpression() { // GIVEN - let pixelReporterMock = OnboardingIntroPixelReporterMock() + let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackOnboardingIntroImpression) @@ -315,7 +315,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStartOnboardingActionIsCalledThenPixelReporterTrackBrowserComparisonImpression() { // GIVEN - let pixelReporterMock = OnboardingIntroPixelReporterMock() + let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackBrowserComparisonImpression) @@ -328,7 +328,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenChooseBrowserIsCalledThenPixelReporterTrackChooseBrowserCTAAction() { // GIVEN - let pixelReporterMock = OnboardingIntroPixelReporterMock() + let pixelReporterMock = OnboardingPixelReporterMock() let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackChooseBrowserCTAAction) @@ -339,12 +339,98 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackChooseBrowserCTAAction) } + // MARK: - Pixel "Highlights" + + func testWhenStateChangesToChooseAppIconThenPixelReporterTrackAppIconImpression() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackBrowserComparisonImpression) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackChooseAppIconImpression) + } + + func testWhenAppIconPickerContinueActionIsCalledAndIconIsCustomColorThenPixelReporterTrackCustomAppIconColor() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), appIconProvider: { .purple }) + XCTAssertFalse(pixelReporterMock.didCallTrackChooseCustomAppIconColor) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackChooseCustomAppIconColor) + } + + func testWhenAppIconPickerContinueActionIsCalledAndIconIsDefaultColorThenPixelReporterDoNotTrackCustomAppIconColor() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), appIconProvider: { .defaultAppIcon }) + XCTAssertFalse(pixelReporterMock.didCallTrackChooseCustomAppIconColor) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertFalse(pixelReporterMock.didCallTrackChooseCustomAppIconColor) + } + + func testWhenStateChangesToChooseAddressBarPositionThenPixelReporterTrackAddressBarSelectionImpression() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + XCTAssertFalse(pixelReporterMock.didCallTrackAddressBarPositionSelectionImpression) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackAddressBarPositionSelectionImpression) + } + + func testWhenSelectAddressBarPositionActionIsCalledAndAddressBarPositionIsBottomThenPixelReporterTrackChooseBottomAddressBarPosition() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), addressBarPositionProvider: { .bottom }) + XCTAssertFalse(pixelReporterMock.didCallTrackChooseBottomAddressBarPosition) + + // WHEN + sut.selectAddressBarPositionAction() + + // THEN + XCTAssertTrue(pixelReporterMock.didCallTrackChooseBottomAddressBarPosition) + } + + func testWhenSelectAddressBarPositionActionIsCalledAndAddressBarPositionIsTopThenPixelReporterDoNotTrackChooseBottomAddressBarPosition() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let pixelReporterMock = OnboardingPixelReporterMock() + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener(), addressBarPositionProvider: { .top }) + XCTAssertFalse(pixelReporterMock.didCallTrackChooseBottomAddressBarPosition) + + // WHEN + sut.selectAddressBarPositionAction() + + // THEN + XCTAssertFalse(pixelReporterMock.didCallTrackChooseBottomAddressBarPosition) + } + // MARK: - Copy func testWhenIsNotHighlightsThenIntroTitleIsCorrect() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.copy.introTitle @@ -356,7 +442,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenIsHighlightsThenIntroTitleIsCorrect() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.copy.introTitle @@ -368,7 +454,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenIsNotHighlightsThenBrowserComparisonTitleIsCorrect() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.copy.browserComparisonTitle @@ -380,7 +466,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenIsHighlightsThenBrowserComparisonTitleIsCorrect() { // GIVEN onboardingManager.isOnboardingHighlightsEnabled = true - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.copy.browserComparisonTitle @@ -390,21 +476,3 @@ final class OnboardingIntroViewModelTests: XCTestCase { } } - -private final class OnboardingIntroPixelReporterMock: OnboardingIntroPixelReporting { - private(set) var didCallTrackOnboardingIntroImpression = false - private(set) var didCallTrackBrowserComparisonImpression = false - private(set) var didCallTrackChooseBrowserCTAAction = false - - func trackOnboardingIntroImpression() { - didCallTrackOnboardingIntroImpression = true - } - - func trackBrowserComparisonImpression() { - didCallTrackBrowserComparisonImpression = true - } - - func trackChooseBrowserCTAAction() { - didCallTrackChooseBrowserCTAAction = true - } -} diff --git a/DuckDuckGoTests/OnboardingPixelReporterMock.swift b/DuckDuckGoTests/OnboardingPixelReporterMock.swift index 855d445fcb..4a4b20a4a5 100644 --- a/DuckDuckGoTests/OnboardingPixelReporterMock.swift +++ b/DuckDuckGoTests/OnboardingPixelReporterMock.swift @@ -22,11 +22,15 @@ import Core import Onboarding @testable import DuckDuckGo -final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, OnboardingSiteSuggestionsPixelReporting, OnboardingSearchSuggestionsPixelReporting, OnboardingCustomInteractionPixelReporting, OnboardingScreenImpressionReporting { +final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, OnboardingSiteSuggestionsPixelReporting, OnboardingSearchSuggestionsPixelReporting, OnboardingCustomInteractionPixelReporting, OnboardingDaxDialogsReporting { private(set) var didCallTrackOnboardingIntroImpression = false private(set) var didCallTrackBrowserComparisonImpression = false - private(set) var didCallChooseBrowserCTAAction = false + private(set) var didCallTrackChooseBrowserCTAAction = false + private(set) var didCallTrackChooseAppIconImpression = false + private(set) var didCallTrackChooseCustomAppIconColor = false + private(set) var didCallTrackAddressBarPositionSelectionImpression = false + private(set) var didCallTrackChooseBottomAddressBarPosition = false private(set) var didCallTrackSearchOptionTapped = false private(set) var didCallTrackSiteOptionTapped = false private(set) var didCallTrackCustomSearch = false @@ -40,6 +44,7 @@ final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, Onboardi private(set) var didCallTrackScreenImpressionCalled = false private(set) var capturedScreenImpression: Pixel.Event? private(set) var didCallTrackPrivacyDashboardOpenedForFirstTime = false + private(set) var didCallTrackEndOfJourneyDialogDismiss = false func trackOnboardingIntroImpression() { didCallTrackOnboardingIntroImpression = true @@ -50,7 +55,27 @@ final class OnboardingPixelReporterMock: OnboardingIntroPixelReporting, Onboardi } func trackChooseBrowserCTAAction() { - didCallChooseBrowserCTAAction = true + didCallTrackChooseBrowserCTAAction = true + } + + func trackChooseAppIconImpression() { + didCallTrackChooseAppIconImpression = true + } + + func trackChooseCustomAppIconColor() { + didCallTrackChooseCustomAppIconColor = true + } + + func trackAddressBarPositionSelectionImpression() { + didCallTrackAddressBarPositionSelectionImpression = true + } + + func trackChooseBottomAddressBarPosition() { + didCallTrackChooseBottomAddressBarPosition = true + } + + func trackEndOfJourneyDialogCTAAction() { + didCallTrackEndOfJourneyDialogDismiss = true } func trackSiteSuggetionOptionTapped() { diff --git a/DuckDuckGoTests/OnboardingPixelReporterTests.swift b/DuckDuckGoTests/OnboardingPixelReporterTests.swift index 09994eb9ce..f79808e3c9 100644 --- a/DuckDuckGoTests/OnboardingPixelReporterTests.swift +++ b/DuckDuckGoTests/OnboardingPixelReporterTests.swift @@ -107,46 +107,6 @@ final class OnboardingPixelReporterTests: XCTestCase { XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) } - // MARK: - List - - func testWhenTrackSearchSuggestionOptionTappedThenSearchOptionTappedFires() { - // GIVEN - let expectedPixel = Pixel.Event.onboardingContextualSearchOptionTappedUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) - - // WHEN - sut.trackSearchSuggetionOptionTapped() - - // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) - XCTAssertEqual(expectedPixel.name, "m_onboarding_search_option_tapped_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) - } - - func testWhenTrackSiteSuggestionThenSiteOptionsTappedFires() { - // GIVEN - let expectedPixel = Pixel.Event.onboardingContextualSiteOptionTappedUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) - - // WHEN - sut.trackSiteSuggetionOptionTapped() - - // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) - XCTAssertEqual(expectedPixel.name, "m_onboarding_visit_site_option_tapped_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) - } - // MARK: - Custom Interactions func testWhenTrackCustomSearchIsCalledThenSearchCustomFires() { @@ -269,7 +229,7 @@ final class OnboardingPixelReporterTests: XCTestCase { XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams["daysSinceInstall"], "3") } - // MARK: - Screen Impressions + // MARK: - Dax Dialogs func testWhenTrackScreenImpressionIsCalledThenPixelFires() { // GIVEN @@ -288,6 +248,23 @@ final class OnboardingPixelReporterTests: XCTestCase { XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) } + func testWhentrackEndOfJourneyDialogCTAActionIsCalledThenDaxDialogsEndOfJourneyDismissedPixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.daxDialogsEndOfJourneyDismissed + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackEndOfJourneyDialogCTAAction() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion, .atb]) + } + // Enqueuing / Dequeuing Pixels func testWhenPixelIsFiredAndAndATBIsNotAvailableAndPixelNeedsATBThenEnqueuePixel() throws { @@ -353,4 +330,75 @@ final class OnboardingPixelReporterTests: XCTestCase { XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEventHistory[0], .onboardingIntroShownUnique) XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEventHistory[1], .onboardingIntroComparisonChartShownUnique) } + + // MARK: - Onboarding Intro Highglights Experiment + + func testWhenTrackChooseAppIconImpressionIsCalledThenOnboardingIntroChooseIconImpressionUniquePixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroChooseAppIconImpressionUnique + XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackChooseAppIconImpression() + + // THEN + XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + } + + func testWhenTrackChooseNonDefaultAppIconIsCalledThenOnboardingIntroChooseCustomIconColorPixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroChooseCustomAppIconColorCTAPressed + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackChooseCustomAppIconColor() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) + } + + func testWhenTrackAddressBarPositionSelectionImpressionIsCalledThenOnboardingIntroChooseAddressBarImpressionUniquePixelFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroChooseAddressBarImpressionUnique + XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackAddressBarPositionSelectionImpression() + + // THEN + XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + } + + func testWhenTrackChooseBottomAddressBarPositionIsCalledThenOnboardingIntroBottomAddressBarSelectedFires() { + // GIVEN + let expectedPixel = Pixel.Event.onboardingIntroBottomAddressBarSelected + XCTAssertFalse(OnboardingPixelFireMock.didCallFire) + XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + + // WHEN + sut.trackChooseBottomAddressBarPosition() + + // THEN + XCTAssertTrue(OnboardingPixelFireMock.didCallFire) + XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertEqual(expectedPixel.name, expectedPixel.name) + XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) + } + } From d77035f99020facfa7a54190b62760bc2d146a85 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 19 Sep 2024 12:37:21 +0200 Subject: [PATCH 16/46] Address autofill security concerns (#3321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414235014887631/1207411921782781/f **Description**: [✓ Implement Survey for Password Manager Users](https://app.asana.com/0/72649045549333/1206568003117818) showed that a proportion of users are hesitant to use our Password Manager because they don't know how secure it is. Easing these concerns should increase the adoption of DuckDuckGo's Password Manager. These changes update and add copy to more clearly explain the security safe-guards of using the Password Manager. **Steps to test this PR**: - Go to the screens from the designs in [Figma](https://www.figma.com/design/wAWx1a0mAooj6sDCmoTFbS/Password-Manager-security?node-id=192-10952&node-type=FRAME&t=0sLE1hdNaQCkC7A8-0) and check they match. Double check Ship Review for any copy divergences. **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**: * [x] Portrait * [ ] Landscape **Device Testing**: * [x] iPhone SE (1st Gen) * [x] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [x] iOS 17 **Theme Testing**: * [x] Light theme * [x] 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/AppURLs.swift | 1 + .../16px/Lock-Solid-16.imageset/Contents.json | 15 +++++++ .../Lock-Solid-16.imageset/Lock-Solid-16.pdf | Bin 0 -> 2602 bytes DuckDuckGo/AutofillItemsEmptyView.swift | 1 - ...ofillLoginSettingsListViewController.swift | 9 ++-- .../AutofillSettingsEnableFooterView.swift | 42 ++++++++++++++---- DuckDuckGo/AutofillViews.swift | 27 ++++++++--- DuckDuckGo/PasswordGenerationPromptView.swift | 2 +- DuckDuckGo/SaveLoginView.swift | 4 +- DuckDuckGo/UserText.swift | 8 +++- DuckDuckGo/en.lproj/Localizable.strings | 17 +++++-- 11 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf diff --git a/Core/AppURLs.swift b/Core/AppURLs.swift index 7851870c26..e07d771f2a 100644 --- a/Core/AppURLs.swift +++ b/Core/AppURLs.swift @@ -37,6 +37,7 @@ public extension URL { static let aboutLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/about"))! static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps"))! static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))! + static let autofillHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/sync-and-backup/password-manager-security/"))! static let surrogates = URL(string: "\(staticBase)/surrogates.txt")! diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json new file mode 100644 index 0000000000..ce371320cc --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Lock-Solid-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1a5584f9c8e20ce5de1f860ce2ce42208ada181 GIT binary patch literal 2602 zcmd^B&rjPh6u$ef@C6AbEwPh0sU=hiTBxQC!P*^=&<-IrrL0Z5B%NS?{hl4?IUz-C z=lKB6=lAn__Io|LnO=V6UP1_`j28D#gwoSf>VJFLDeB)|-#@9W1`L+}Mdhpdkp>_* zx6rIC_PdUP`EJgPPNTR0du5teyR=k!_R>2akFBZ2{=PO&FN<<(vAL>>eVM6ByFkhF z^WvG_QRXqT+=d4&Sl`m;|K;ev_4a7FUwKaiQXY=|94P>rH7_GMTIw=OF5Z5Ig>eW&-3=u1@PXvyD)*n~)|Gb6`Qo{r zE$XyZ=KO-2e~YWM`eAm>T+yGsDd2`hf9KDrzH)Zl^v?!gKFpc3&F(RM-*h`m1~xQc zhaYD0mbvk`NgIr{?<4LpWd~-`GW1tN$Oq0=74d=EX>s^%YJ*Lcq8MF|5Y$ieEYFL& zqIY`yxs-0wm9oH UIView? { switch viewModel.viewState { - case .empty: - return viewModel.sections[section] == .enableAutofill ? enableAutofillFooterView : nil - case .showItems: + case .showItems, .empty: return viewModel.sections[section] == .enableAutofill ? enableAutofillFooterView : nil default: return nil diff --git a/DuckDuckGo/AutofillSettingsEnableFooterView.swift b/DuckDuckGo/AutofillSettingsEnableFooterView.swift index aa2011be22..1d2f26d26f 100644 --- a/DuckDuckGo/AutofillSettingsEnableFooterView.swift +++ b/DuckDuckGo/AutofillSettingsEnableFooterView.swift @@ -18,6 +18,7 @@ // import UIKit +import DesignResourcesKit class AutofillSettingsEnableFooterView: UIView { @@ -36,16 +37,33 @@ class AutofillSettingsEnableFooterView: UIView { fatalError("init(coder:) has not been implemented") } - private lazy var title: UILabel = { - let label = UILabel(frame: CGRect.zero) - label.font = .preferredFont(forTextStyle: .footnote) - label.numberOfLines = 0 - label.textAlignment = .left - label.lineBreakMode = .byWordWrapping - label.textColor = UIColor(designSystemColor: .textSecondary) - label.text = UserText.autofillSettingsFooter + private lazy var title: UITextView = { + let textView = UITextView(frame: CGRect.zero) + textView.delegate = self + textView.textAlignment = .left - return label + var attributedText = NSMutableAttributedString() + let attributedTextDescription = (try? NSMutableAttributedString(markdown: UserText.autofillLoginListSettingsFooterMarkdown)) ?? NSMutableAttributedString(string: UserText.autofillLoginListSettingsFooterFallback) + let attachment = NSTextAttachment() + attachment.image = UIImage(resource: .lockSolid16).withTintColor(UIColor(designSystemColor: .textSecondary)) + attachment.bounds = CGRect(x: 0, y: -1, width: 12, height: 12) + let attributedTextImage = NSMutableAttributedString(attachment: attachment) + attributedText.append(attributedTextImage) + attributedText.append(.init(string: " ")) + attributedText.append(attributedTextDescription) + let wholeRange = NSRange(location: 0, length: attributedText.length) + attributedText.addAttribute(.foregroundColor, value: UIColor(designSystemColor: .textSecondary), range: wholeRange) + attributedText.addAttribute(.font, value: UIFont.daxFootnoteRegular(), range: wholeRange) + + textView.attributedText = attributedText + textView.linkTextAttributes = [.foregroundColor: UIColor(designSystemColor: .accent)] + textView.isEditable = false + textView.isScrollEnabled = false + textView.backgroundColor = .clear + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + + return textView }() private func installSubviews() { @@ -67,3 +85,9 @@ class AutofillSettingsEnableFooterView: UIView { ]) } } + +extension AutofillSettingsEnableFooterView: UITextViewDelegate { + func textViewDidChangeSelection(_ textView: UITextView) { + textView.selectedTextRange = nil + } +} diff --git a/DuckDuckGo/AutofillViews.swift b/DuckDuckGo/AutofillViews.swift index ffb219210c..5ea7061702 100644 --- a/DuckDuckGo/AutofillViews.swift +++ b/DuckDuckGo/AutofillViews.swift @@ -82,11 +82,28 @@ struct AutofillViews { var body: some View { Text(text) - .daxFootnoteRegular() - .foregroundColor(Color(designSystemColor: .textSecondary)) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: Const.Size.maxWidth) + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: Const.Size.maxWidth) + } + } + + struct SecureDescription: View { + let text: String + + var body: some View { + ( + Text("\(Image(.lockSolid16)) ").baselineOffset(-1.0) + + + Text(text) + ) + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: Const.Size.maxWidth) } } diff --git a/DuckDuckGo/PasswordGenerationPromptView.swift b/DuckDuckGo/PasswordGenerationPromptView.swift index 6743d47f12..ed257a27d4 100644 --- a/DuckDuckGo/PasswordGenerationPromptView.swift +++ b/DuckDuckGo/PasswordGenerationPromptView.swift @@ -56,7 +56,7 @@ struct PasswordGenerationPromptView: View { passwordView AutofillViews.LegacySpacerView() } - AutofillViews.Description(text: UserText.autofillPasswordGenerationPromptSubtitle) + AutofillViews.SecureDescription(text: UserText.autofillSaveLoginSecurityMessage) contentViewSpacer ctaView .padding(.bottom, AutofillViews.isIPad(verticalSizeClass, horizontalSizeClass) ? Const.Size.bottomPaddingIPad diff --git a/DuckDuckGo/SaveLoginView.swift b/DuckDuckGo/SaveLoginView.swift index 601441e663..9d15b185d4 100644 --- a/DuckDuckGo/SaveLoginView.swift +++ b/DuckDuckGo/SaveLoginView.swift @@ -216,8 +216,8 @@ struct SaveLoginView: View { private var contentView: some View { switch layoutType { case .newUser, .saveLogin, .savePassword, .updatePassword: - let text = layoutType == .updatePassword ? UserText.autoUpdatePasswordMessage : UserText.autofillSaveLoginMessageNewUser - AutofillViews.Description(text: text) + let text = layoutType == .updatePassword ? UserText.autoUpdatePasswordMessage : UserText.autofillSaveLoginSecurityMessage + AutofillViews.SecureDescription(text: text) case .updateUsername: updateUsernameContentView } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 0f6a604a29..b586b9f55d 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -396,7 +396,7 @@ public struct UserText { public static let autofillSaveLoginTitle = NSLocalizedString("autofill.save-login.title", value: "Save password?", comment: "Title displayed on modal asking for the user to save the login") public static let autofillUpdateUsernameTitle = NSLocalizedString("autofill.update-usernamr.title", value: "Update username?", comment: "Title displayed on modal asking for the user to update the username") - public static let autofillSaveLoginMessageNewUser = NSLocalizedString("autofill.save-login.new-user.message", value: "DuckDuckGo Passwords & Autofill stores passwords securely on your device.", comment: "Message displayed on modal asking for the user to save the login for the first time") + public static let autofillSaveLoginSecurityMessage = NSLocalizedString("autofill.save-login.security.message", value: "Securely store your password on device with DuckDuckGo Passwords & Autofill.", comment: "Message displayed on modal asking for the user to save the login for the first time") public static let autofillSaveLoginNeverPromptCTA = NSLocalizedString("autofill.save-login.never-prompt.CTA", value: "Never Ask for This Site", comment: "CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin") public static func autofillUpdatePassword(for title: String) -> String { @@ -701,9 +701,11 @@ public struct UserText { public static let autofillLoginDetailsAddress = NSLocalizedString("autofill.logins.details.address", value:"Website URL", comment: "Address label for login details on autofill") public static let autofillLoginDetailsNotes = NSLocalizedString("autofill.logins.details.notes", value:"Notes", comment: "Notes label for login details on autofill") public static let autofillEmptyViewTitle = NSLocalizedString("autofill.logins.empty-view.title", value:"No passwords saved yet", comment: "Title for view displayed when autofill has no items") - public static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.empty-view.subtitle", value:"Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser.", comment: "Subtitle for view displayed when no autofill passwords have been saved") + public static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.empty-view.subtitle.first.paragraph", value:"You can import saved passwords from another browser into DuckDuckGo.", comment: "Subtitle for view displayed when no autofill passwords have been saved") public static let autofillEmptyViewButtonTitle = NSLocalizedString("autofill.logins.empty-view.button.title", value:"Import Passwords", comment: "Title for button to Import Passwords when autofill has no items") + public static let autofillLearnMoreLinkTitle = NSLocalizedString("autofill.learn.more.link.title", value: "Learn More", comment: "A link that takes the user to the DuckDuckGo help pages explaining password managers") + public static let autofillSearchNoResultTitle = NSLocalizedString("autofill.logins.search.no-results.title", value:"No Results", comment: "Title displayed when there are no results on Autofill search") public static func autofillSearchNoResultSubtitle(for query: String) -> String { let message = NSLocalizedString("autofill.logins.search.no-results.subtitle", value: "for '%@'", comment: "Subtitle displayed when there are no results on Autofill search, example : No Result (Title) for Duck (Subtitle)") @@ -726,6 +728,8 @@ But if you *do* want a peek under the hood, you can find more information about public static let autofillLoginListTitle = NSLocalizedString("autofill.logins.list.title", value:"Passwords", comment: "Title for screen listing autofill logins") public static let autofillLoginListSearchPlaceholder = NSLocalizedString("autofill.logins.list.search-placeholder", value:"Search passwords", comment: "Placeholder for search field on autofill login listing") public static let autofillLoginListSuggested = NSLocalizedString("autofill.logins.list.suggested", value:"Suggested", comment: "Section title for group of suggested saved logins") + public static let autofillLoginListSettingsFooterMarkdown = NSLocalizedString("autofill.logins.list.settings.footer.markdown", value: "Passwords are encrypted. Nobody but you can see them, not even us. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)", comment: "Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More.") + public static let autofillLoginListSettingsFooterFallback = NSLocalizedString("autofill.logins.list.settings.footer.fallback", value: "Passwords are encrypted. Nobody but you can see them, not even us.", comment: "Subtext under Autofill Settings briefly explaining security to alleviate user concerns.") public static let autofillResetNeverSavedActionTitle = NSLocalizedString("autofill.logins.list.never.saved.reset.action.title", value:"If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites.", comment: "Alert title") public static let autofillResetNeverSavedActionConfirmButton = NSLocalizedString("autofill.logins.list.never.saved.reset.action.confirm", value: "Reset Excluded Sites", comment: "Confirm button to reset list of never saved sites") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 1bdd754039..affd90745c 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -322,6 +322,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Disable"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "View"; @@ -416,7 +419,7 @@ "autofill.logins.empty-view.button.title" = "Import Passwords"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "You can import saved passwords from another browser into DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "No passwords saved yet"; @@ -460,6 +463,12 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Search passwords"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Passwords are encrypted. Nobody but you can see them, not even us."; + +/* 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" = "Passwords are encrypted. Nobody but you can see them, not even us. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Suggested"; @@ -578,12 +587,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Never Ask for This Site"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Passwords & Autofill stores passwords securely on your device."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Save password?"; From 158d9285e28d9dfd3d4078ef08e76f5791b28ec8 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Thu, 19 Sep 2024 12:49:03 -0500 Subject: [PATCH 17/46] Balance calls for file presenter (#3372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414709148257752/1208349795584311/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. See https://github.com/duckduckgo/BrowserServicesKit/pull/999 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) --- DuckDuckGo/Configuration/ConfigurationManager.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index c9e494f992..a34f161f01 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -75,12 +75,10 @@ final class ConfigurationManager: DefaultConfigurationManager { store: ConfigurationStoring = AppDependencyProvider.shared.configurationStore, defaults: KeyValueStoring = UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { super.init(fetcher: fetcher, store: store, defaults: defaults) - addPresenter() subscribeToLifecycleNotifications() } deinit { - removePresenter() removeLifecycleNotifications() } @@ -135,7 +133,7 @@ final class ConfigurationManager: DefaultConfigurationManager { return didFetchAnyTrackerBlockingDependencies } - + private func updateTrackerBlockingDependencies() { ContentBlocking.shared.privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), data: store.loadData(for: .privacyConfiguration)) From 9478841dffc8f9c7a1a2720deb7a3669b4c6af18 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 20 Sep 2024 09:21:41 +0200 Subject: [PATCH 18/46] Makes AppDependencyProvider non-instantiatable (#3373) Task/Issue URL: https://app.asana.com/0/414235014887631/1208353288582614/f ## Description This is a follow up to [NetworkProtectionTunnelController leaking](https://app.asana.com/0/inbox/1203108348814444/1208331423616717/1208348963925370) We want to make AppDependencyProvider non instantiatable, since it's really not meant to be. --- DuckDuckGo/AppDependencyProvider.swift | 12 +++++++++--- DuckDuckGoTests/AutoClearSettingsScreenTests.swift | 2 +- DuckDuckGoTests/LargeOmniBarStateTests.swift | 2 +- DuckDuckGoTests/MockDependencyProvider.swift | 2 +- DuckDuckGoTests/SmallOmniBarStateTests.swift | 2 +- ...ubscriptionPagesUseSubscriptionFeatureTests.swift | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index e0840dde47..c25532703b 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -54,7 +54,7 @@ protocol DependencyProvider { /// Provides dependencies for objects that are not directly instantiated /// through `init` call (e.g. ViewControllers created from Storyboards). -class AppDependencyProvider: DependencyProvider { +final class AppDependencyProvider: DependencyProvider { static var shared: DependencyProvider = AppDependencyProvider() @@ -89,12 +89,12 @@ class AppDependencyProvider: DependencyProvider { let networkProtectionTunnelController: NetworkProtectionTunnelController let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) - + let connectionObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession() let serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession() let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) - init() { + private init() { featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) @@ -144,4 +144,10 @@ class AppDependencyProvider: DependencyProvider { vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, accountManager: accountManager) } + + /// Only meant to be used for testing. + /// + static func makeTestingInstance() -> Self { + Self.init() + } } diff --git a/DuckDuckGoTests/AutoClearSettingsScreenTests.swift b/DuckDuckGoTests/AutoClearSettingsScreenTests.swift index e4ce725474..95d056cf4a 100644 --- a/DuckDuckGoTests/AutoClearSettingsScreenTests.swift +++ b/DuckDuckGoTests/AutoClearSettingsScreenTests.swift @@ -35,7 +35,7 @@ class AutoClearSettingsScreenTests: XCTestCase { override func tearDown() { super.tearDown() - AppDependencyProvider.shared = AppDependencyProvider() + AppDependencyProvider.shared = AppDependencyProvider.makeTestingInstance() } func testWhenOpeningSettingsThenClearDataToggleIsSetBasedOnAppSettings() { diff --git a/DuckDuckGoTests/LargeOmniBarStateTests.swift b/DuckDuckGoTests/LargeOmniBarStateTests.swift index 0df7041e5b..d237d7ff96 100644 --- a/DuckDuckGoTests/LargeOmniBarStateTests.swift +++ b/DuckDuckGoTests/LargeOmniBarStateTests.swift @@ -36,7 +36,7 @@ class LargeOmniBarStateTests: XCTestCase { override func tearDown() { super.tearDown() - AppDependencyProvider.shared = AppDependencyProvider() + AppDependencyProvider.shared = AppDependencyProvider.makeTestingInstance() } func testWhenInHomeEmptyEditingStateWithoutVoiceSearchThenCorrectButtonsAreShown() { diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index f0df5f6a4a..a1e7a7e5bd 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -51,7 +51,7 @@ class MockDependencyProvider: DependencyProvider { var vpnSettings: NetworkProtection.VPNSettings init() { - let defaultProvider = AppDependencyProvider() + let defaultProvider = AppDependencyProvider.makeTestingInstance() appSettings = defaultProvider.appSettings variantManager = defaultProvider.variantManager featureFlagger = defaultProvider.featureFlagger diff --git a/DuckDuckGoTests/SmallOmniBarStateTests.swift b/DuckDuckGoTests/SmallOmniBarStateTests.swift index d7b2f0d3e1..8eb0ceb1ef 100644 --- a/DuckDuckGoTests/SmallOmniBarStateTests.swift +++ b/DuckDuckGoTests/SmallOmniBarStateTests.swift @@ -36,7 +36,7 @@ class SmallOmniBarStateTests: XCTestCase { override func tearDown() { super.tearDown() - AppDependencyProvider.shared = AppDependencyProvider() + AppDependencyProvider.shared = AppDependencyProvider.makeTestingInstance() } func testWhenInHomeEmptyEditingStateWithoutVoiceSearchThenCorrectButtonsAreShown() { diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index a3b80166f4..d51d38feca 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -169,7 +169,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { pixelsFired.removeAll() HTTPStubs.removeAllStubs() - AppDependencyProvider.shared = AppDependencyProvider() + AppDependencyProvider.shared = AppDependencyProvider.makeTestingInstance() subscriptionService = nil authService = nil From 92da4318103d61d6a2bac7950c4b639b3b3b9e30 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Fri, 20 Sep 2024 18:49:25 +0200 Subject: [PATCH 19/46] Remove autofill.deduplicateLoginsOnImport feature flag (#3369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1203822806345703/1207759292115042/f **Description**: Just a BSK bump to remove a feature flag that wasn't used in iOS. **Steps to test this PR**: 1. Just CI **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) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 11934ff00d..192ec061e5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10927,7 +10927,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 196.2.0; + version = 197.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cf5b1aae12..8ef30ff3b2 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" : "32a2ec64385543ccfbaaafbfe9545543a2c06aac", - "version" : "196.2.0" + "revision" : "40f2fcc23944e028e16798a784ceff7e24ba6683", + "version" : "197.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" From 3845520f61020da4ae9b4fbc1d78adff5416059d Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Sun, 22 Sep 2024 19:02:45 +0100 Subject: [PATCH 20/46] Add tabs to suggestions/autocomplete on iOS (#3371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/392891325557410/1208036752520497/f Tech Design URL: CC: **Description**: Adds open tabs to suggestions. I'm going to follow up with localisations and UI tests in a separate PR as I want to get this thing merged with BSK and can update iOS internal easily on its own. **Steps to test this PR**: 1. Open a bunch of sites and bookmark some of them (quick way to do this is via Hacker News Ycombinator then go through and open the first 5 sites in the background and use tab switcher to bookmark them all, then go back and open a few more from the list) 2. Open a new tab and type until you get a suggestion for an open tab 3. Switching to the open tab should close the new tab 4. Matches that have bookmarks and tabs should show both, but history should not be shown in these cases 5. Edit a bookmark so that its title starts with quotes and is something different to its URL. Check that bookmark is still suggested based on the first word in the title 6. Close all tabs (not fire) and type until you see history for sites that have not been bookmarked 7. Use the fire button 8. Typing should not show any history but should show matching bookmarks **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [x] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [x] Portrait * [ ] Landscape **Device Testing**: * [x] iPhone SE (1st Gen) * [x] iPhone 8 * [x] iPhone X * [x] iPhone 14 Pro * [x] iPad **OS Testing**: * [x] iOS 15 * [x] iOS 16 * [x] iOS 17 **Theme Testing**: * [x] Light theme * [x] 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/BookmarksCachingSearch.swift | 99 ++++++++++--------- Core/FeatureFlag.swift | 3 + Core/PixelEvent.swift | 4 + DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../24px/OpenTab-24.imageset/Contents.json | 15 +++ .../24px/OpenTab-24.imageset/OpenTab-24.svg | 3 + DuckDuckGo/AutocompleteView.swift | 6 ++ DuckDuckGo/AutocompleteViewController.swift | 51 +++++++++- DuckDuckGo/MainViewController.swift | 18 +++- DuckDuckGo/SuggestionTrayViewController.swift | 12 ++- DuckDuckGo/UserText.swift | 1 + DuckDuckGo/en.lproj/Localizable.strings | 3 + .../BookmarksCachingSearchTests.swift | 63 +++++++++--- 14 files changed, 218 insertions(+), 68 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg diff --git a/Core/BookmarksCachingSearch.swift b/Core/BookmarksCachingSearch.swift index 089eec2699..a219442954 100644 --- a/Core/BookmarksCachingSearch.swift +++ b/Core/BookmarksCachingSearch.swift @@ -133,12 +133,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch { self.title = title self.url = url self.isFavorite = isFavorite - - if isFavorite { - score = 0 - } else { - score = -1 - } + self.score = 0 } init?(bookmark: [String: Any]) { @@ -191,7 +186,6 @@ public class BookmarksCachingSearch: BookmarksStringSearch { return cachedBookmarksAndFavorites } - // swiftlint:disable cyclomatic_complexity private func score(query: String, input: [ScoredBookmark]) -> [ScoredBookmark] { let query = query.lowercased() let tokens = query.split(separator: " ").filter { !$0.isEmpty }.map { String($0).lowercased() } @@ -201,55 +195,63 @@ public class BookmarksCachingSearch: BookmarksStringSearch { for index in 0.. 0 { + result.append(input[index]) } + } + return result + } - let domain = entry.url.host?.droppingWwwPrefix() ?? "" + private func score(_ query: String, _ bookmark: ScoredBookmark, _ tokens: [String]) -> Int { + let title = bookmark.title.lowercased() + let domain = bookmark.url.host?.droppingWwwPrefix() ?? "" + var score = bookmark.isFavorite ? 0 : -1 - // Tokenized matches + // Exact matches - full query + if title.leadingBoundaryStartsWith(query) { // High score for exact match from the beginning of the title + score += 200 + } else if title.contains(" \(query)") { // Exact match from the beginning of the word within string. + score += 100 + } - if tokens.count > 1 { - var matchesAllTokens = true - for token in tokens { - // Match only from the beginning of the word to avoid unintuitive matches. - if !title.starts(with: token) && !title.contains(" \(token)") && !domain.starts(with: token) { - matchesAllTokens = false - break - } + // Tokenized matches + + if tokens.count > 1 { + var matchesAllTokens = true + for token in tokens { + // Match only from the beginning of the word to avoid unintuitive matches. + if !title.leadingBoundaryStartsWith(token) && + !title.contains(" \(token)") + && !domain.starts(with: token) { + matchesAllTokens = false + break } + } - if matchesAllTokens { - // Score tokenized matches - input[index].score += 10 - - // Boost score if first token matches: - if let firstToken = tokens.first { // domain - high score boost - if domain.starts(with: firstToken) { - input[index].score += 300 - } else if title.starts(with: firstToken) { // beginning of the title - moderate score boost - input[index].score += 50 - } + if matchesAllTokens { + // Score tokenized matches + score += 10 + + // Boost score if first token matches: + if let firstToken = tokens.first { // domain - high score boost + if domain.starts(with: firstToken) { + score += 300 + } else if title.leadingBoundaryStartsWith(firstToken) { // beginning of the title - moderate score boost + score += 50 } } - } else { - // High score for matching domain in the URL - if let firstToken = tokens.first, domain.starts(with: firstToken) { - input[index].score += 300 - } } - if input[index].score > 0 { - result.append(input[index]) + } else { + // High score for matching domain in the URL + if let firstToken = tokens.first, domain.starts(with: firstToken) { + score += 300 } } - return result + + return score } - // swiftlint:enable cyclomatic_complexity public func search(query: String) -> [BookmarksStringSearchResult] { guard hasData else { @@ -265,3 +267,12 @@ public class BookmarksCachingSearch: BookmarksStringSearch { return finalResult } } + +private extension String { + + /// e.g. "Cats and Dogs" would match `Cats` or `"Cats` + func leadingBoundaryStartsWith(_ s: String) -> Bool { + return starts(with: s) || trimmingCharacters(in: .alphanumerics.inverted).starts(with: s) + } + +} diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 763b282091..6d5b363635 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -42,6 +42,7 @@ public enum FeatureFlag: String { case syncPromotionPasswords case onboardingHighlights case autofillSurveys + case autcompleteTabs } extension FeatureFlag: FeatureFlagSourceProviding { @@ -89,6 +90,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .autofillSurveys: return .remoteReleasable(.feature(.autofillSurveys)) + case .autcompleteTabs: + return .remoteReleasable(.feature(.autocompleteTabs)) } } } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2f67e300f1..d7ff07905b 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -131,9 +131,11 @@ extension Pixel { case autocompleteClickFavorite case autocompleteClickSearchHistory case autocompleteClickSiteHistory + case autocompleteClickOpenTab case autocompleteDisplayedLocalBookmark case autocompleteDisplayedLocalFavorite case autocompleteDisplayedLocalHistory + case autocompleteDisplayedOpenedTab case autocompleteSwipeToDelete case feedbackPositive @@ -946,9 +948,11 @@ extension Pixel.Event { case .autocompleteClickFavorite: return "m_autocomplete_click_favorite" case .autocompleteClickSearchHistory: return "m_autocomplete_click_history_search" case .autocompleteClickSiteHistory: return "m_autocomplete_click_history_site" + case .autocompleteClickOpenTab: return "m_autocomplete_click_switch_to_tab" case .autocompleteDisplayedLocalBookmark: return "m_autocomplete_display_local_bookmark" case .autocompleteDisplayedLocalFavorite: return "m_autocomplete_display_local_favorite" case .autocompleteDisplayedLocalHistory: return "m_autocomplete_display_local_history" + case .autocompleteDisplayedOpenedTab: return "m_autocomplete_display_switch_to_tab" case .autocompleteSwipeToDelete: return "m_autocomplete_result_deleted" case .feedbackPositive: return "mfbs_positive_submit" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 192ec061e5..c1fa3fd356 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10927,7 +10927,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 197.0.0; + version = 198.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8ef30ff3b2..db741141be 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" : "40f2fcc23944e028e16798a784ceff7e24ba6683", - "version" : "197.0.0" + "revision" : "6e1520bd83bbcc269b0d561c51fc92b81fe6d93b", + "version" : "198.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json new file mode 100644 index 0000000000..e83ff99786 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "OpenTab-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg new file mode 100644 index 0000000000..f00822f378 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg @@ -0,0 +1,3 @@ + + + diff --git a/DuckDuckGo/AutocompleteView.swift b/DuckDuckGo/AutocompleteView.swift index f138157461..3003807655 100644 --- a/DuckDuckGo/AutocompleteView.swift +++ b/DuckDuckGo/AutocompleteView.swift @@ -249,6 +249,11 @@ private struct SuggestionView: View { title: title ?? "", subtitle: url.formattedForSuggestion()) + case .openTab(title: let title, url: let url): + SuggestionListItem(icon: Image("OpenTab-24"), + title: title, + subtitle: "\(UserText.autocompleteSwitchToTab) · \(url.formattedForSuggestion())") + case .internalPage, .unknown: FailedAssertionView("Unknown or unsupported suggestion type") } @@ -336,6 +341,7 @@ private extension URL { let string = absoluteString .dropping(prefix: "https://") .dropping(prefix: "http://") + .droppingWwwPrefix() return pathComponents.isEmpty ? string : string.dropping(suffix: "/") } diff --git a/DuckDuckGo/AutocompleteViewController.swift b/DuckDuckGo/AutocompleteViewController.swift index 0c680525da..7c09c4da6a 100644 --- a/DuckDuckGo/AutocompleteViewController.swift +++ b/DuckDuckGo/AutocompleteViewController.swift @@ -58,19 +58,33 @@ class AutocompleteViewController: UIHostingController { CachedBookmarks(bookmarksDatabase) }() + private lazy var openTabs: [BrowserTab] = { + tabsModel.tabs.compactMap { + guard let url = $0.link?.url else { return nil } + return OpenTab(title: $0.link?.displayTitle ?? "", url: url) + } + }() + private var lastResults: SuggestionResult? private var loader: SuggestionLoader? - private var historyMessageManager: HistoryMessageManager + private var tabsModel: TabsModel + private var featureFlagger: FeatureFlagger init(historyManager: HistoryManaging, bookmarksDatabase: CoreDataDatabase, appSettings: AppSettings, - historyMessageManager: HistoryMessageManager = HistoryMessageManager()) { + historyMessageManager: HistoryMessageManager = HistoryMessageManager(), + tabsModel: TabsModel, + featureFlagger: FeatureFlagger) { + + self.tabsModel = tabsModel self.historyManager = historyManager self.bookmarksDatabase = bookmarksDatabase self.appSettings = appSettings self.historyMessageManager = historyMessageManager + self.featureFlagger = featureFlagger + self.model = AutocompleteViewModel(isAddressBarAtBottom: appSettings.currentAddressBarPosition == .bottom, showMessage: historyManager.isHistoryFeatureEnabled() && historyMessageManager.shouldShow()) super.init(rootView: AutocompleteView(model: model)) @@ -119,6 +133,7 @@ class AutocompleteViewController: UIHostingController { var bookmark = false var favorite = false var history = false + var openTab = false lastResults?.all.forEach { switch $0 { @@ -132,6 +147,9 @@ class AutocompleteViewController: UIHostingController { case .historyEntry: history = true + case .openTab: + openTab = true + default: break } } @@ -148,6 +166,10 @@ class AutocompleteViewController: UIHostingController { Pixel.fire(pixel: .autocompleteDisplayedLocalHistory) } + if openTab { + Pixel.fire(pixel: .autocompleteDisplayedOpenedTab) + } + } private func cancelInFlightRequests() { @@ -158,7 +180,7 @@ class AutocompleteViewController: UIHostingController { private func requestSuggestions(query: String) { model.selection = nil - loader = SuggestionLoader(dataSource: self, urlFactory: { phrase in + loader = SuggestionLoader(urlFactory: { phrase in guard let url = URL(trimmedAddressBarString: phrase), let scheme = url.scheme, scheme.description.hasPrefix("http"), @@ -169,7 +191,7 @@ class AutocompleteViewController: UIHostingController { return url }) - loader?.getSuggestions(query: query) { [weak self] result, error in + loader?.getSuggestions(query: query, usingDataSource: self) { [weak self] result, error in guard let self, error == nil else { return } let updatedResults = result ?? .empty self.lastResults = updatedResults @@ -228,6 +250,9 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate { case .website: Pixel.fire(pixel: .autocompleteClickWebsite) + case .openTab: + Pixel.fire(pixel: .autocompleteClickOpenTab) + default: // NO-OP break @@ -259,6 +284,10 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate { extension AutocompleteViewController: SuggestionLoadingDataSource { + var platform: Platform { + .mobile + } + func history(for suggestionLoading: Suggestions.SuggestionLoading) -> [HistorySuggestion] { return historyCoordinator.history ?? [] } @@ -271,6 +300,13 @@ extension AutocompleteViewController: SuggestionLoadingDataSource { return [] } + func openTabs(for suggestionLoading: any SuggestionLoading) -> [BrowserTab] { + if featureFlagger.isFeatureOn(.autcompleteTabs) { + return openTabs + } + return [] + } + func suggestionLoading(_ suggestionLoading: Suggestions.SuggestionLoading, suggestionDataFromUrl url: URL, withParameters parameters: [String: String], completion: @escaping (Data?, Error?) -> Void) { var queryURL = url parameters.forEach { @@ -298,3 +334,10 @@ extension HistoryEntry: HistorySuggestion { } } + +struct OpenTab: BrowserTab { + + let title: String + let url: URL + +} diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 12e5ae1e7b..dd9e7efeb5 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -398,7 +398,9 @@ class MainViewController: UIViewController { SuggestionTrayViewController(coder: coder, favoritesViewModel: self.favoritesViewModel, bookmarksDatabase: self.bookmarksDatabase, - historyManager: self.historyManager) + historyManager: self.historyManager, + tabsModel: self.tabManager.model, + featureFlagger: self.featureFlagger) }) else { assertionFailure() return @@ -2088,16 +2090,26 @@ extension MainViewController: AutocompleteViewControllerDelegate { } else { Logger.lifecycle.error("Couldn‘t form URL for suggestion: \(phrase, privacy: .public)") } + case .website(url: let url): if url.isBookmarklet() { executeBookmarklet(url) } else { loadUrl(url) } + case .bookmark(_, url: let url, _, _): loadUrl(url) + case .historyEntry(_, url: let url, _): loadUrl(url) + + case .openTab(title: _, url: let url): + if homeViewController != nil, let tab = tabManager.model.currentTab { + self.closeTab(tab) + } + loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: .noAttribution) + case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } @@ -2119,6 +2131,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { viewCoordinator.omniBar.textField.text = title case .historyEntry(title: let title, _, _): viewCoordinator.omniBar.textField.text = title + case .openTab: break // no-op case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } @@ -2136,7 +2149,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { } case .website(url: let url): viewCoordinator.omniBar.textField.text = url.absoluteString - case .bookmark(title: let title, _, _, _): + case .bookmark(title: let title, _, _, _), .openTab(title: let title, url: _): viewCoordinator.omniBar.textField.text = title if title.hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) @@ -2149,6 +2162,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { if (title ?? url.absoluteString).hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) } + case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } diff --git a/DuckDuckGo/SuggestionTrayViewController.swift b/DuckDuckGo/SuggestionTrayViewController.swift index be3bc7a0c5..f71ee490f5 100644 --- a/DuckDuckGo/SuggestionTrayViewController.swift +++ b/DuckDuckGo/SuggestionTrayViewController.swift @@ -23,6 +23,7 @@ import Bookmarks import Suggestions import Persistence import History +import BrowserServicesKit class SuggestionTrayViewController: UIViewController { @@ -50,6 +51,8 @@ class SuggestionTrayViewController: UIViewController { private let bookmarksDatabase: CoreDataDatabase private let favoritesModel: FavoritesListInteracting private let historyManager: HistoryManaging + private let tabsModel: TabsModel + private let featureFlagger: FeatureFlagger var selectedSuggestion: Suggestion? { autocompleteController?.selectedSuggestion @@ -79,10 +82,12 @@ class SuggestionTrayViewController: UIViewController { } } - required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging) { + required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, tabsModel: TabsModel, featureFlagger: FeatureFlagger) { self.favoritesModel = favoritesViewModel self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager + self.tabsModel = tabsModel + self.featureFlagger = featureFlagger super.init(coder: coder) } @@ -236,8 +241,9 @@ class SuggestionTrayViewController: UIViewController { private func installAutocompleteSuggestions() { let controller = AutocompleteViewController(historyManager: historyManager, bookmarksDatabase: bookmarksDatabase, - appSettings: appSettings) - + appSettings: appSettings, + tabsModel: tabsModel, + featureFlagger: featureFlagger) install(controller: controller) controller.delegate = autocompleteDelegate controller.presentationDelegate = self diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index b586b9f55d..c8c2fe0910 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1251,6 +1251,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let autocompleteHistoryWarningTitle = NSLocalizedString("autocomplete.history.warning.title", value: "Same privacy.\nBetter search suggestions!", comment: "Title for message show in suggestions") public static let autocompleteHistoryWarningDescription = NSLocalizedString("autocomplete.history.warning.message", value: "Search suggestions now include your recently visited sites. Turn off in Settings, or clear anytime with the 🔥 Fire Button.", comment: "The message text shown in suggestions") public static let autocompleteSearchDuckDuckGo = NSLocalizedString("autocomplete.history.search.duckduckgo", value: "Search DuckDuckGo", comment: "Subtitle for search history items") + public static let autocompleteSwitchToTab = NSLocalizedString("autocomplete.switch.to.tab", value: "Switch to Tab", comment: "Switch to tab hint") // Site not working public static let siteNotWorkingTitle = NSLocalizedString("site.not.working.title", value: "Site not working? Let DuckDuckGo know.", comment: "Prompt asking user to send report to us if we suspect site may be broken") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index affd90745c..2d4b428f71 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -250,6 +250,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Same privacy.\nBetter search suggestions!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Switch to Tab"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Disabled"; diff --git a/DuckDuckGoTests/BookmarksCachingSearchTests.swift b/DuckDuckGoTests/BookmarksCachingSearchTests.swift index 03f79cb6d2..21545ad57e 100644 --- a/DuckDuckGoTests/BookmarksCachingSearchTests.swift +++ b/DuckDuckGoTests/BookmarksCachingSearchTests.swift @@ -46,7 +46,8 @@ class BookmarksCachingSearchTests: XCTestCase { let simpleStore = MockBookmarksSearchStore() let urlStore = MockBookmarksSearchStore() - + let quotedTitleStore = MockBookmarksSearchStore() + enum Entry: String { case b1 = "bookmark test 1" case b2 = "test bookmark 2" @@ -61,6 +62,9 @@ class BookmarksCachingSearchTests: XCTestCase { case urlExample2 = "Test E 2" case urlNasa = "Test N 1 Duck" case urlDDG = "Test D 1" + + case quotedTitle1 = "\"Cats and Dogs\"" + case quotedTitle2 = "«Рукописи не горят»: первый замысел" } private var mockObjectID: NSManagedObjectID! @@ -75,20 +79,29 @@ class BookmarksCachingSearchTests: XCTestCase { mockObjectID = BookmarkUtils.fetchRootFolder(inMemoryStore.viewContext)?.objectID XCTAssertNotNil(mockObjectID) - simpleStore.dataSet = [BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b1.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b2.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12a.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f1.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f2.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12a.rawValue, url: url, isFavorite: true)] - + simpleStore.dataSet = [ + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b1.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b2.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12a.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f1.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f2.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12a.rawValue, url: url, isFavorite: true), + ] + urlStore.dataSet = [ BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlExample1.rawValue, url: URL(string: "https://example.com")!, isFavorite: true), BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlExample2.rawValue, url: URL(string: "https://example.com")!, isFavorite: true), BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlNasa.rawValue, url: URL(string: "https://www.nasa.gov")!, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlDDG.rawValue, url: url, isFavorite: true)] + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlDDG.rawValue, url: url, isFavorite: true), + ] + + quotedTitleStore.dataSet = [ + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.quotedTitle1.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.quotedTitle2.rawValue, url: url, isFavorite: false), + ] + } override func tearDown() { @@ -98,6 +111,34 @@ class BookmarksCachingSearchTests: XCTestCase { super.tearDown() } + func testWhenSearchingForCharactersThenCharactersAtTheStartAreMatched() async throws { + let engine = BookmarksCachingSearch(bookmarksStore: quotedTitleStore) + var bookmarks = engine.search(query: "\"") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "«") + XCTAssertEqual(bookmarks.count, 1) + } + + func testWhenSearchingForWordsAtStartWithQuotesThenWordsAreMatched() async throws { + + let engine = BookmarksCachingSearch(bookmarksStore: quotedTitleStore) + var bookmarks = engine.search(query: "Cats") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Р") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Ру") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Рук") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Nope") + XCTAssertEqual(bookmarks.count, 0) + } + func testWhenSearchingThenOnlyBeginingsOfWordsAreMatched() async throws { let engine = BookmarksCachingSearch(bookmarksStore: simpleStore) From 37d25582bf77fd75f7c5699ece251c522c365f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 23 Sep 2024 11:35:39 +0200 Subject: [PATCH 21/46] Show Add Favorite placeholder as last favorite item on New Tab Page (#3375) Task/Issue URL: https://app.asana.com/0/72649045549333/1208244619690578/f Tech Design URL: CC: **Description**: Adds a possibility to show an Add button in favorites collection, allowing to create a favorite right from New Tab Page. ([Figma](https://www.figma.com/design/N2GbF5HEvopp5iwmAlMwyD/New-Tab-Page-Customization?node-id=611-147700&m=dev)). Add button is currently navigating to an empty view, which is expected. The view will be implemented in additional PR. In order to reduce the amount of duplicated code and separate additional display elements from pure Favorites data, DataSource for Favorites was defined which is used in common ViewModel logic for Favorites, hence the rename of `NewTabPageModel` -> `NewTabPageViewModel`. There was also a need to keep some of the items immovable, which required a change in `ReorderableForEach`. **Steps to test this PR**: 1. Open NTP without Favorites, + button should be visible as a first element. 2. Add a couple of Favorites, + button should be visible as last item. 3. Try reordering Favorites, + button should be kept stationary and it shouldn't be posible to drop favorite after it. **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.xcodeproj/project.pbxproj | 54 +++++---- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../AddFavoritePlaceholderItemView.swift | 38 ++++++ DuckDuckGo/EditableShortcutsView.swift | 8 +- DuckDuckGo/FavoriteDataSource.swift | 95 +++++++++++++++ DuckDuckGo/FavoriteItem.swift | 50 ++++++++ ....swift => FavoritesDefaultViewModel.swift} | 111 ++++++++---------- DuckDuckGo/FavoritesEmptyStateView.swift | 29 ++++- DuckDuckGo/FavoritesPreviewDataSource.swift | 77 ++++++++++++ DuckDuckGo/FavoritesSectionHeader.swift | 2 +- DuckDuckGo/FavoritesView.swift | 89 +++++++++----- ...esModel.swift => FavoritesViewModel.swift} | 8 +- DuckDuckGo/NewTabPageView.swift | 55 ++++++--- DuckDuckGo/NewTabPageViewController.swift | 17 +-- ...eModel.swift => NewTabPageViewModel.swift} | 4 +- DuckDuckGo/ReorderableForEach.swift | 74 ++++++------ DuckDuckGo/ToggleExpandButtonStyle.swift | 15 +-- DuckDuckGo/ViewExtension.swift | 15 --- .../NewTabPageFavoritesModelTests.swift | 46 +++++--- ...s.swift => NewTabPageViewModelTests.swift} | 18 +-- .../DuckUI/Sources/DuckUI/Button.swift | 13 +- .../ViewExtensions/ViewModifyExtension.swift | 35 ++++++ LocalPackages/SyncUI/Package.swift | 2 +- LocalPackages/Waitlist/Package.swift | 2 +- 24 files changed, 615 insertions(+), 246 deletions(-) create mode 100644 DuckDuckGo/AddFavoritePlaceholderItemView.swift create mode 100644 DuckDuckGo/FavoriteDataSource.swift create mode 100644 DuckDuckGo/FavoriteItem.swift rename DuckDuckGo/{FavoritesDefaultModel.swift => FavoritesDefaultViewModel.swift} (70%) create mode 100644 DuckDuckGo/FavoritesPreviewDataSource.swift rename DuckDuckGo/{FavoritesModel.swift => FavoritesViewModel.swift} (89%) rename DuckDuckGo/{NewTabPageModel.swift => NewTabPageViewModel.swift} (96%) rename DuckDuckGoTests/{NewTabPageModelTests.swift => NewTabPageViewModelTests.swift} (79%) create mode 100644 LocalPackages/DuckUI/Sources/DuckUI/ViewExtensions/ViewModifyExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c1fa3fd356..272857cd89 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -329,24 +329,25 @@ 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */; }; 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */; }; 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */; }; - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultModel.swift */; }; + 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */; }; 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; }; 6FD0C41F2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */; }; - 6FD0C4212C5BF774000561C9 /* NewTabPageModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C4202C5BF774000561C9 /* NewTabPageModelTests.swift */; }; + 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */; }; 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */; }; 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */; }; 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */; }; - 6FD3F8112C3EFCDB00DA5797 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8102C3EFCDB00DA5797 /* FavoritesModel.swift */; }; - 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */; }; + 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */; }; + 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */; }; 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */; }; 6FD8E51E2C5B84DE00345670 /* NewTabPageIntroMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */; }; - 6FD8E5202C5BA23200345670 /* NewTabPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51F2C5BA23200345670 /* NewTabPageModel.swift */; }; + 6FD8E5202C5BA23200345670 /* NewTabPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51F2C5BA23200345670 /* NewTabPageViewModel.swift */; }; 6FD8E5222C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E5212C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift */; }; 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */; }; 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */; }; + 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */; }; 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127372C20492500EB5724 /* NewTabPage.swift */; }; @@ -356,6 +357,8 @@ 6FE127432C204DF700EB5724 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */; }; 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */; }; 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */; }; + 6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */; }; + 6FEC0B882C999961006B4F6E /* FavoriteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */; }; 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; @@ -1606,24 +1609,25 @@ 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteEmptyStateItem.swift; sourceTree = ""; }; 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesEmptyStateView.swift; sourceTree = ""; }; 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageGridView.swift; sourceTree = ""; }; - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDefaultModel.swift; sourceTree = ""; }; + 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDefaultViewModel.swift; sourceTree = ""; }; 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetupTests.swift; sourceTree = ""; }; - 6FD0C4202C5BF774000561C9 /* NewTabPageModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageModelTests.swift; sourceTree = ""; }; + 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewModelTests.swift; sourceTree = ""; }; 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = AdAttribution/AdAttributionPixelReporter.swift; sourceTree = ""; }; 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = AdAttribution/AdAttributionReporterStorage.swift; sourceTree = ""; }; 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOrientationEnvironmentValue.swift; sourceTree = ""; }; - 6FD3F8102C3EFCDB00DA5797 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = ""; }; - 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewModel.swift; sourceTree = ""; }; + 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; + 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewDataSource.swift; sourceTree = ""; }; 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDelegate.swift; sourceTree = ""; }; 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageView.swift; sourceTree = ""; }; - 6FD8E51F2C5BA23200345670 /* NewTabPageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageModel.swift; sourceTree = ""; }; + 6FD8E51F2C5BA23200345670 /* NewTabPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewModel.swift; sourceTree = ""; }; 6FD8E5212C5BA5C400345670 /* NewTabPageIntroMessageSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetup.swift; sourceTree = ""; }; 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroDataStoring.swift; sourceTree = ""; }; 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStore.swift; sourceTree = ""; }; + 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFavoritePlaceholderItemView.swift; sourceTree = ""; }; 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FE127372C20492500EB5724 /* NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPage.swift; sourceTree = ""; }; @@ -1633,6 +1637,8 @@ 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItemView.swift; sourceTree = ""; }; 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewController.swift; sourceTree = ""; }; 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutItemView.swift; sourceTree = ""; }; + 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = ""; }; + 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteDataSource.swift; sourceTree = ""; }; 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = ""; }; 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = ""; }; @@ -3833,7 +3839,7 @@ 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */, 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */, 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */, - 6FD0C4202C5BF774000561C9 /* NewTabPageModelTests.swift */, + 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */, 6F7FB8DF2C660B1A00867DA7 /* NewTabPageFavoritesModelTests.swift */, 6F7FB8E42C66158D00867DA7 /* NewTabPageShortcutsSettingsModelTests.swift */, 6F7FB8E62C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift */, @@ -3887,11 +3893,13 @@ 6FA3438D2C3D3BB800470677 /* Model */ = { isa = PBXGroup; children = ( - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultModel.swift */, + 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */, 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */, - 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewModel.swift */, - 6FD3F8102C3EFCDB00DA5797 /* FavoritesModel.swift */, + 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, + 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, + 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, + 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, ); name = Model; sourceTree = ""; @@ -3901,6 +3909,7 @@ children = ( 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */, 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */, + 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */, ); name = Item; sourceTree = ""; @@ -3949,7 +3958,7 @@ 6FE1273B2C204C0D00EB5724 /* Subviews */, 6F03CAF82C32C3AA004179A8 /* Messages */, 6FE127372C20492500EB5724 /* NewTabPage.swift */, - 6FD8E51F2C5BA23200345670 /* NewTabPageModel.swift */, + 6FD8E51F2C5BA23200345670 /* NewTabPageViewModel.swift */, 6FE127392C204BD000EB5724 /* NewTabPageView.swift */, 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */, 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */, @@ -7467,7 +7476,7 @@ C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */, 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */, 982E5630222C3D5B008D861B /* FeedbackPickerViewController.swift in Sources */, - 6FD8E5202C5BA23200345670 /* NewTabPageModel.swift in Sources */, + 6FD8E5202C5BA23200345670 /* NewTabPageViewModel.swift in Sources */, 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */, 85DFEDED24C7CCA500973FE7 /* AppWidthObserver.swift in Sources */, 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */, @@ -7567,7 +7576,7 @@ 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, - 6FD3F8112C3EFCDB00DA5797 /* FavoritesModel.swift in Sources */, + 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckPlayerNavigationHandling.swift in Sources */, 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, @@ -7598,6 +7607,7 @@ D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, C1641EB12BC2F52B0012607A /* ImportPasswordsView.swift in Sources */, CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */, + 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */, 982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */, F446B9B5251150AC00324016 /* HomeMessageViewSectionRenderer.swift in Sources */, D6E0C1852B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift in Sources */, @@ -7687,7 +7697,7 @@ 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultModel.swift in Sources */, + 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -7806,7 +7816,7 @@ 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, 3712091E2C21E390003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, - 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewModel.swift in Sources */, + 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift in Sources */, 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, @@ -7836,8 +7846,10 @@ 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */, 37C696772C4957940073E131 /* RemoteMessagingDebugViewController.swift in Sources */, 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */, + 6FEC0B882C999961006B4F6E /* FavoriteDataSource.swift in Sources */, F4F6DFB626E6B71300ED7E12 /* BookmarkFoldersTableViewController.swift in Sources */, 8586A11024CCCD040049720E /* TabsBarViewController.swift in Sources */, + 6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */, F1D796F41E7C2A410019D451 /* BookmarksDelegate.swift in Sources */, D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */, 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, @@ -7883,7 +7895,7 @@ 1CB7B82323CEA28300AA24EA /* DateExtensionTests.swift in Sources */, 31C138A427A3352600FFD4B2 /* DownloadTests.swift in Sources */, 853A717820F645FB00FE60BC /* PixelTests.swift in Sources */, - 6FD0C4212C5BF774000561C9 /* NewTabPageModelTests.swift in Sources */, + 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */, 984D036124AF49B80066CFB8 /* TabPreviewsSourceTests.swift in Sources */, 85D2187024BF24DB004373D2 /* FaviconRequestModifierTests.swift in Sources */, EAB19EDA268963510015D3EA /* DomainMatchingTests.swift in Sources */, @@ -10967,7 +10979,7 @@ repositoryURL = "https://github.com/duckduckgo/DesignResourcesKit"; requirement = { kind = exactVersion; - version = 3.2.0; + version = 3.3.0; }; }; F486D2EF25069482002D07D7 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index db741141be..fd1bb75388 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/DesignResourcesKit", "state" : { - "revision" : "fe3c383b5e21e0a419e0f979f7f2cd4e67385b15", - "version" : "3.2.0" + "revision" : "ad133f76501edcb2bfa841e33aebc0da5f92bb5c", + "version" : "3.3.0" } }, { diff --git a/DuckDuckGo/AddFavoritePlaceholderItemView.swift b/DuckDuckGo/AddFavoritePlaceholderItemView.swift new file mode 100644 index 0000000000..09b11802c0 --- /dev/null +++ b/DuckDuckGo/AddFavoritePlaceholderItemView.swift @@ -0,0 +1,38 @@ +// +// AddFavoritePlaceholderItemView.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 DesignResourcesKit + +struct AddFavoritePlaceholderItemView: View { + var body: some View { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(.clear) + .overlay { + Image(.add24) + .tintIfAvailable(Color(designSystemColor: .icons)) + } + .aspectRatio(1.0, contentMode: .fit) + } +} + +#Preview { + AddFavoritePlaceholderItemView() + .frame(width: 100) +} diff --git a/DuckDuckGo/EditableShortcutsView.swift b/DuckDuckGo/EditableShortcutsView.swift index 1ceca96f54..08ac409016 100644 --- a/DuckDuckGo/EditableShortcutsView.swift +++ b/DuckDuckGo/EditableShortcutsView.swift @@ -61,12 +61,12 @@ private extension View { extension NewTabPageSettingsModel.NTPSetting: Reorderable, Hashable, Equatable { - var dropItemProvider: NSItemProvider { - NSItemProvider(object: item.id as NSString) + var trait: ReorderableTrait { + let itemProvider = NSItemProvider(object: item.id as NSString) + let metadata = MoveMetadata(itemProvider: itemProvider, type: .text) + return .movable(metadata) } - var dropType: UTType { .text } - static func == (lhs: Self, rhs: Self) -> Bool { lhs.item == rhs.item } diff --git a/DuckDuckGo/FavoriteDataSource.swift b/DuckDuckGo/FavoriteDataSource.swift new file mode 100644 index 0000000000..921938d532 --- /dev/null +++ b/DuckDuckGo/FavoriteDataSource.swift @@ -0,0 +1,95 @@ +// +// FavoriteDataSource.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import Bookmarks + +final class FavoritesListInteractingAdapter: NewTabPageFavoriteDataSource { + + let favoritesListInteracting: FavoritesListInteracting + + init(favoritesListInteracting: FavoritesListInteracting) { + self.favoritesListInteracting = favoritesListInteracting + } + + var externalUpdates: AnyPublisher { favoritesListInteracting.externalUpdates } + + var favorites: [Favorite] { + (try? favoritesListInteracting.favorites.map(Favorite.init)) ?? [] + } + + func moveFavorite(_ favorite: Favorite, fromIndex: Int, toIndex: Int) { + guard let entity = bookmarkEntity(for: favorite) else { return } + + // adjust for different target index handling + let toIndex = toIndex > fromIndex ? toIndex - 1 : toIndex + favoritesListInteracting.moveFavorite(entity, fromIndex: fromIndex, toIndex: toIndex) + } + + func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? { + favoritesListInteracting.favorites.first { + $0.uuid == favorite.id + } + } + + func favorite(at index: Int) throws -> Favorite? { + try favoritesListInteracting.favorite(at: index).map(Favorite.init) + } + + func removeFavorite(_ favorite: Favorite) { + guard let entity = bookmarkEntity(for: favorite) else { return } + + favoritesListInteracting.removeFavorite(entity) + } +} + +private extension Favorite { + init(_ bookmark: BookmarkEntity) throws { + guard let uuid = bookmark.uuid else { + throw FavoriteMappingError.missingUUID + } + + self.id = uuid + self.title = bookmark.displayTitle + self.domain = bookmark.host + self.urlObject = bookmark.urlObject + } +} + +private extension BookmarkEntity { + + var displayTitle: String { + if let title = title?.trimmingWhitespace(), !title.isEmpty { + return title + } + + if let host = urlObject?.host?.droppingWwwPrefix() { + return host + } + + assertionFailure("Unable to create display title") + return "" + } + + var host: String { + return urlObject?.host ?? "" + } + +} diff --git a/DuckDuckGo/FavoriteItem.swift b/DuckDuckGo/FavoriteItem.swift new file mode 100644 index 0000000000..9e6c433d53 --- /dev/null +++ b/DuckDuckGo/FavoriteItem.swift @@ -0,0 +1,50 @@ +// +// FavoriteItem.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UniformTypeIdentifiers + +enum FavoriteItem { + case favorite(Favorite) + case addFavorite +} + +extension FavoriteItem: Identifiable { + var id: String { + switch self { + case .favorite(let favorite): + return favorite.id + case .addFavorite: + return "addFavorite" + } + } +} + +extension FavoriteItem: Reorderable { + var trait: ReorderableTrait { + switch self { + case .favorite(let favorite): + let itemProvider = NSItemProvider(object: (favorite.urlObject?.absoluteString ?? "") as NSString) + let metadata = MoveMetadata(itemProvider: itemProvider, type: .plainText) + return .movable(metadata) + case .addFavorite: + return .stationary + } + } +} diff --git a/DuckDuckGo/FavoritesDefaultModel.swift b/DuckDuckGo/FavoritesDefaultViewModel.swift similarity index 70% rename from DuckDuckGo/FavoritesDefaultModel.swift rename to DuckDuckGo/FavoritesDefaultViewModel.swift index 92cf6e3ab3..c9088dc5e7 100644 --- a/DuckDuckGo/FavoritesDefaultModel.swift +++ b/DuckDuckGo/FavoritesDefaultViewModel.swift @@ -1,5 +1,5 @@ // -// FavoritesDefaultModel.swift +// FavoritesDefaultViewModel.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -24,9 +24,22 @@ import SwiftUI import Core import WidgetKit -final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { +protocol NewTabPageFavoriteDataSource { + var externalUpdates: AnyPublisher { get } + var favorites: [Favorite] { get } - @Published private(set) var allFavorites: [Favorite] = [] + func moveFavorite(_ favorite: Favorite, + fromIndex: Int, + toIndex: Int) + + func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? + func favorite(at index: Int) throws -> Favorite? + func removeFavorite(_ favorite: Favorite) +} + +class FavoritesDefaultViewModel: FavoritesViewModel, FavoritesEmptyStateModel { + + @Published private(set) var allFavorites: [FavoriteItem] = [] @Published private(set) var isCollapsed: Bool = true @Published private(set) var isShowingTooltip: Bool = false @@ -34,19 +47,19 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { private var cancellables = Set() - private let interactionModel: FavoritesListInteracting + private let favoriteDataSource: NewTabPageFavoriteDataSource private let pixelFiring: PixelFiring.Type private let dailyPixelFiring: DailyPixelFiring.Type var isEmpty: Bool { - allFavorites.isEmpty + allFavorites.filter(\.isFavorite).isEmpty } - init(interactionModel: FavoritesListInteracting, + init(favoriteDataSource: NewTabPageFavoriteDataSource, faviconLoader: FavoritesFaviconLoading, pixelFiring: PixelFiring.Type = Pixel.self, dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { - self.interactionModel = interactionModel + self.favoriteDataSource = favoriteDataSource self.pixelFiring = pixelFiring self.dailyPixelFiring = dailyPixelFiring self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in @@ -58,15 +71,11 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { }) - interactionModel.externalUpdates.sink { [weak self] _ in - try? self?.updateData() + favoriteDataSource.externalUpdates.sink { [weak self] _ in + self?.updateData() }.store(in: &cancellables) - do { - try updateData() - } catch { - fatalError(error.localizedDescription) - } + updateData() } func toggleCollapse() { @@ -107,21 +116,21 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { var onFavoriteDeleted: ((BookmarkEntity) -> Void)? func deleteFavorite(_ favorite: Favorite) { - guard let entity = lookupEntity(for: favorite) else { return } + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) - interactionModel.removeFavorite(entity) + favoriteDataSource.removeFavorite(favorite) WidgetCenter.shared.reloadAllTimelines() - try? updateData() + updateData() onFavoriteDeleted?(entity) } var onFavoriteEdit: ((BookmarkEntity) -> Void)? func editFavorite(_ favorite: Favorite) { - guard let entity = lookupEntity(for: favorite) else { return } + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) @@ -132,12 +141,10 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { guard indexSet.count == 1, let fromIndex = indexSet.first else { return } - let favorite = allFavorites[fromIndex] - guard let entity = lookupEntity(for: favorite) else { return } + let favoriteItem = allFavorites[fromIndex] + guard case let .favorite(favorite) = favoriteItem else { return } - // adjust for different target index handling - let toIndex = index > fromIndex ? index - 1 : index - interactionModel.moveFavorite(entity, fromIndex: fromIndex, toIndex: toIndex) + favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) } @@ -156,14 +163,13 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { // MARK: - - private func lookupEntity(for favorite: Favorite) -> BookmarkEntity? { - interactionModel.favorites.first { - $0.uuid == favorite.id + private func updateData() { + var allFavorites = favoriteDataSource.favorites.map { + FavoriteItem.favorite($0) } - } - - private func updateData() throws { - self.allFavorites = try interactionModel.favorites.map(Favorite.init) + allFavorites.append(.addFavorite) + + self.allFavorites = allFavorites } } @@ -171,40 +177,6 @@ enum FavoriteMappingError: Error { case missingUUID } -private extension Favorite { - init(_ bookmark: BookmarkEntity) throws { - guard let uuid = bookmark.uuid else { - throw FavoriteMappingError.missingUUID - } - - self.id = uuid - self.title = bookmark.displayTitle - self.domain = bookmark.host - self.urlObject = bookmark.urlObject - } -} - -private extension BookmarkEntity { - - var displayTitle: String { - if let title = title?.trimmingWhitespace(), !title.isEmpty { - return title - } - - if let host = urlObject?.host?.droppingWwwPrefix() { - return host - } - - assertionFailure("Unable to create display title") - return "" - } - - var host: String { - return urlObject?.host ?? "" - } - -} - private final class MissingFaviconWrapper: FavoritesFaviconLoading { let loader: FavoritesFaviconLoading @@ -233,3 +205,14 @@ private final class MissingFaviconWrapper: FavoritesFaviconLoading { loader.existingFavicon(for: favorite, size: size) } } + +private extension FavoriteItem { + var isFavorite: Bool { + switch self { + case .favorite: + return true + case .addFavorite: + return false + } + } +} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift index 15db8ab2ca..8c81ce6765 100644 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ b/DuckDuckGo/FavoritesEmptyStateView.swift @@ -18,12 +18,11 @@ // import SwiftUI +import DuckUI struct FavoritesEmptyStateView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.isLandscapeOrientation) var isLandscape - @ObservedObject var model: Model + @Binding var isAddingFavorite: Bool let geometry: GeometryProxy? @@ -33,7 +32,15 @@ struct FavoritesEmptyStateView: View { FavoritesSectionHeader(model: model) NewTabPageGridView(geometry: geometry) { placeholdersCount in - let placeholders = Array(0..: View { } #Preview { - return FavoritesEmptyStateView(model: FavoritesPreviewModel(), geometry: nil) + PreviewViewWrapper() +} + +private struct PreviewViewWrapper: View { + @State var isAddingFavorite = false + + var body: some View { + FavoritesEmptyStateView( + model: FavoritesPreviewModel(), + isAddingFavorite: $isAddingFavorite, + geometry: nil + ) + } } diff --git a/DuckDuckGo/FavoritesPreviewDataSource.swift b/DuckDuckGo/FavoritesPreviewDataSource.swift new file mode 100644 index 0000000000..c9ecacaca2 --- /dev/null +++ b/DuckDuckGo/FavoritesPreviewDataSource.swift @@ -0,0 +1,77 @@ +// +// FavoritesPreviewDataSource.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 Combine +import Bookmarks +import Foundation + +final class FavoritesPreviewModel: FavoritesDefaultViewModel { + init(favorites: [Favorite] = randomFavorites) { + super.init(favoriteDataSource: FavoritesPreviewDataSource(favorites: favorites), faviconLoader: EmptyFaviconLoading()) + } + + static var randomFavorites: [Favorite] { + (0...20).map { + Favorite( + id: UUID().uuidString, + title: "Favorite \($0)", + domain: "favorite\($0).domain.com") + } + } +} + +final class FavoritesPreviewDataSource: NewTabPageFavoriteDataSource { + var externalUpdates: AnyPublisher = Empty().eraseToAnyPublisher() + + var favorites: [Favorite] + + init(favorites: [Favorite]) { + self.favorites = favorites + } + + func moveFavorite(_ favorite: Favorite, fromIndex: Int, toIndex: Int) { + favorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex) + } + + func bookmarkEntity(for favorite: Favorite) -> Bookmarks.BookmarkEntity? { + nil + } + + func favorite(at index: Int) throws -> Favorite? { + favorites[index] + } + + func removeFavorite(_ favorite: Favorite) { + // no-op + } +} + +struct EmptyFaviconLoading: FavoritesFaviconLoading { + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + nil + } + + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + .empty + } + + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + nil + } +} diff --git a/DuckDuckGo/FavoritesSectionHeader.swift b/DuckDuckGo/FavoritesSectionHeader.swift index 4a9d744e74..6e14b9ad06 100644 --- a/DuckDuckGo/FavoritesSectionHeader.swift +++ b/DuckDuckGo/FavoritesSectionHeader.swift @@ -45,5 +45,5 @@ struct FavoritesSectionHeader: View { } #Preview { - return FavoritesSectionHeader(model: FavoritesPreviewModel()) + FavoritesSectionHeader(model: FavoritesPreviewModel()) } diff --git a/DuckDuckGo/FavoritesView.swift b/DuckDuckGo/FavoritesView.swift index d5fcc55d9a..2d640bffad 100644 --- a/DuckDuckGo/FavoritesView.swift +++ b/DuckDuckGo/FavoritesView.swift @@ -20,12 +20,14 @@ import Bookmarks import SwiftUI import UniformTypeIdentifiers +import DuckUI -struct FavoritesView: View { +struct FavoritesView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.isLandscapeOrientation) var isLandscape @ObservedObject var model: Model + @Binding var isAddingFavorite: Bool let geometry: GeometryProxy? private let selectionFeedback = UISelectionFeedbackGenerator() @@ -39,29 +41,11 @@ struct FavoritesView: View { NewTabPageGridView(geometry: geometry) { _ in ReorderableForEach(result.items) { item in - Button(action: { - model.favoriteSelected(item) - selectionFeedback.selectionChanged() - }, label: { - FavoriteItemView( - favorite: item, - faviconLoading: model.faviconLoader, - onMenuAction: { action in - switch action { - case .delete: model.deleteFavorite(item) - case .edit: model.editFavorite(item) - } - }) - .background(.clear) - .frame(width: NewTabPageGrid.Item.edgeSize) - }) - .previewShape() - .transition(.opacity) - } preview: { favorite in - FavoriteIconView(favorite: favorite, faviconLoading: model.faviconLoader) - .frame(width: NewTabPageGrid.Item.edgeSize) + viewFor(item) .previewShape() .transition(.opacity) + } preview: { item in + previewFor(item) } onMove: { from, to in haptics.impactOccurred() withAnimation { @@ -88,6 +72,50 @@ struct FavoritesView: View { .clipped() .padding(0) } + + @ViewBuilder + private func previewFor(_ item: FavoriteItem) -> some View { + switch item { + case .favorite(let favorite): + FavoriteIconView(favorite: favorite, faviconLoading: model.faviconLoader) + .frame(width: NewTabPageGrid.Item.edgeSize) + .previewShape() + .transition(.opacity) + case .addFavorite: + EmptyView() + } + } + + @ViewBuilder + private func viewFor(_ item: FavoriteItem) -> some View { + switch item { + case .favorite(let favorite): + Button(action: { + model.favoriteSelected(favorite) + selectionFeedback.selectionChanged() + }, label: { + FavoriteItemView( + favorite: favorite, + faviconLoading: model.faviconLoader, + onMenuAction: { action in + switch action { + case .delete: model.deleteFavorite(favorite) + case .edit: model.editFavorite(favorite) + } + }) + .background(.clear) + .frame(width: NewTabPageGrid.Item.edgeSize) + }) + case .addFavorite: + Button(action: { + isAddingFavorite = true + }, label: { + AddFavoritePlaceholderItemView() + }) + .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) + .frame(width: NewTabPageGrid.Item.edgeSize) + } + } } private extension View { @@ -96,16 +124,13 @@ private extension View { } } -extension Favorite: Reorderable { - var dropItemProvider: NSItemProvider { - NSItemProvider(object: (urlObject?.absoluteString ?? "") as NSString) - } - - var dropType: UTType { - .plainText - } +#Preview { + PreviewWrapperView() } -#Preview { - FavoritesView(model: FavoritesPreviewModel(), geometry: nil) +private struct PreviewWrapperView: View { + @State var isAddingFavorite = false + var body: some View { + FavoritesView(model: FavoritesPreviewModel(), isAddingFavorite: $isAddingFavorite, geometry: nil) + } } diff --git a/DuckDuckGo/FavoritesModel.swift b/DuckDuckGo/FavoritesViewModel.swift similarity index 89% rename from DuckDuckGo/FavoritesModel.swift rename to DuckDuckGo/FavoritesViewModel.swift index ce1804fce2..624186edb6 100644 --- a/DuckDuckGo/FavoritesModel.swift +++ b/DuckDuckGo/FavoritesViewModel.swift @@ -1,5 +1,5 @@ // -// FavoritesModel.swift +// FavoritesViewModel.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -19,8 +19,8 @@ import Foundation -protocol FavoritesModel: AnyObject, ObservableObject { - var allFavorites: [Favorite] { get } +protocol FavoritesViewModel: AnyObject, ObservableObject { + var allFavorites: [FavoriteItem] { get } var faviconLoader: FavoritesFaviconLoading? { get } var isEmpty: Bool { get } @@ -49,6 +49,6 @@ protocol FavoritesEmptyStateModel: AnyObject, ObservableObject { } struct FavoritesSlice { - let items: [Favorite] + let items: [FavoriteItem] let isCollapsible: Bool } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index e8a5b8584f..14d97ce3e2 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -21,10 +21,10 @@ import SwiftUI import DuckUI import RemoteMessaging -struct NewTabPageView: View { +struct NewTabPageView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject private var newTabPageModel: NewTabPageModel + @ObservedObject private var viewModel: NewTabPageViewModel @ObservedObject private var messagesModel: NewTabPageMessagesModel @ObservedObject private var favoritesModel: FavoritesModelType @ObservedObject private var shortcutsModel: ShortcutsModel @@ -32,14 +32,15 @@ struct NewTabPageView some View { Group { if favoritesModel.isEmpty { - FavoritesEmptyStateView(model: favoritesModel, geometry: proxy) + FavoritesEmptyStateView(model: favoritesModel, + isAddingFavorite: $isAddingFavorite, + geometry: proxy) .padding(.top, Metrics.nonGridSectionTopPadding) } else { - FavoritesView(model: favoritesModel, geometry: proxy) + FavoritesView(model: favoritesModel, + isAddingFavorite: $isAddingFavorite, + geometry: proxy) } } } @@ -200,7 +208,7 @@ private extension NewTabPageView { Spacer() Button(action: { - newTabPageModel.customizeNewTabPage() + viewModel.customizeNewTabPage() }, label: { NewTabPageCustomizeButtonView() // Needed to reduce default button margins @@ -211,14 +219,14 @@ private extension NewTabPageView { @ViewBuilder private var introMessageView: some View { - if newTabPageModel.isIntroMessageVisible { + if viewModel.isIntroMessageVisible { NewTabPageIntroMessageView(onClose: { withAnimation { - newTabPageModel.dismissIntroMessage() + viewModel.dismissIntroMessage() } }) .onFirstAppear { - newTabPageModel.introMessageDisplayed() + viewModel.introMessageDisplayed() } .transition(.scale.combined(with: .opacity)) } @@ -260,7 +268,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { #Preview("Regular") { NewTabPageView( - newTabPageModel: NewTabPageModel(), + viewModel: NewTabPageViewModel(), messagesModel: NewTabPageMessagesModel( homePageMessagesConfiguration: PreviewMessagesConfiguration( homeMessages: [] @@ -275,7 +283,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { #Preview("With message") { NewTabPageView( - newTabPageModel: NewTabPageModel(), + viewModel: NewTabPageViewModel(), messagesModel: NewTabPageMessagesModel( homePageMessagesConfiguration: PreviewMessagesConfiguration( homeMessages: [ @@ -298,9 +306,24 @@ private struct CustomizeButtonPrefKey: PreferenceKey { ) } -#Preview("Empty state") { +#Preview("No favorites") { + NewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesModel: FavoritesPreviewModel(favorites: []), + shortcutsModel: ShortcutsModel(), + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), + sectionsSettingsModel: NewTabPageSectionsSettingsModel() + ) +} + +#Preview("Empty") { NewTabPageView( - newTabPageModel: NewTabPageModel(), + viewModel: NewTabPageViewModel(), messagesModel: NewTabPageMessagesModel( homePageMessagesConfiguration: PreviewMessagesConfiguration( homeMessages: [] diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index 7e154ff281..fabd1ee3ed 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -23,7 +23,7 @@ import Bookmarks import BrowserServicesKit import Core -final class NewTabPageViewController: UIHostingController>, NewTabPage { +final class NewTabPageViewController: UIHostingController>, NewTabPage { private let syncService: DDGSyncing private let syncBookmarksAdapter: SyncBookmarksAdapter @@ -33,9 +33,9 @@ final class NewTabPageViewController: UIHostingController: View { typealias ContentBuilder = (Data) -> Content typealias PreviewBuilder = (Data) -> Preview private let data: [Data] - private let isReorderingEnabled: Bool private let id: KeyPath private let content: ContentBuilder @@ -42,12 +36,10 @@ struct ReorderableForEach, - isReorderingEnabled: Bool = true, @ViewBuilder content: @escaping ContentBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { self.data = data self.id = id - self.isReorderingEnabled = isReorderingEnabled self.content = content self.preview = nil self.onMove = onMove @@ -61,7 +53,6 @@ struct ReorderableForEach Void) { self.data = data self.id = id - self.isReorderingEnabled = isReorderingEnabled self.content = content self.preview = preview self.onMove = onMove @@ -69,31 +60,38 @@ struct ReorderableForEach some View { + switch item.trait { + case .stationary: + content(item) + case .movable(let metadata): + if let preview { + droppableContent(for: item, metadata: metadata) + .onDrag { + movedItem = item + return metadata.itemProvider + } preview: { + preview(item) + } } else { - content(item) + droppableContent(for: item, metadata: metadata) + .onDrag { + movedItem = item + return metadata.itemProvider + } } } } - private func droppableContent(for item: Data) -> some View { + @ViewBuilder + private func droppableContent(for item: Data, metadata: MoveMetadata) -> some View { content(item) - .onDrop(of: [item.dropType], delegate: ReorderDropDelegate( + .onDrop(of: [metadata.type], delegate: ReorderDropDelegate( data: data, item: item, onMove: onMove, @@ -129,33 +127,43 @@ private struct ReorderDropDelegate: DropDelegate { func performDrop(info: DropInfo) -> Bool { movedItem = nil - return info.hasItemsConforming(to: [item.dropType]) + return true } } extension ReorderableForEach where Data: Identifiable, ID == Data.ID { init(_ data: [Data], - isReorderingEnabled: Bool = true, @ViewBuilder content: @escaping ContentBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { self.data = data self.id = \Data.id - self.isReorderingEnabled = isReorderingEnabled self.content = content self.preview = nil self.onMove = onMove } init(_ data: [Data], - isReorderingEnabled: Bool = true, @ViewBuilder content: @escaping ContentBuilder, @ViewBuilder preview: @escaping PreviewBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { self.data = data self.id = \Data.id - self.isReorderingEnabled = isReorderingEnabled self.content = content self.preview = preview self.onMove = onMove } } + +struct MoveMetadata { + var itemProvider: NSItemProvider + var type: UTType +} + +enum ReorderableTrait { + case stationary + case movable(MoveMetadata) +} + +protocol Reorderable: Hashable { + var trait: ReorderableTrait { get } +} diff --git a/DuckDuckGo/ToggleExpandButtonStyle.swift b/DuckDuckGo/ToggleExpandButtonStyle.swift index b36c9d2d33..bb8221a3fd 100644 --- a/DuckDuckGo/ToggleExpandButtonStyle.swift +++ b/DuckDuckGo/ToggleExpandButtonStyle.swift @@ -18,13 +18,12 @@ // import SwiftUI -import DuckUI +import DesignResourcesKit struct ToggleExpandButtonStyle: ButtonStyle { - @Environment(\.colorScheme) private var colorScheme - + func makeBody(configuration: Configuration) -> some View { - let isDark = colorScheme == .dark + let backgroundColor = configuration.isPressed ? Color(designSystemColor: .buttonsSecondaryFillPressed) : Color(designSystemColor: .buttonsSecondaryFillDefault) HStack(spacing: 0) { VStack { @@ -34,17 +33,13 @@ struct ToggleExpandButtonStyle: ButtonStyle { Circle() .stroke(Color(designSystemColor: .lines), lineWidth: 1) .frame(width: 32, height: 32) - .if(configuration.isPressed) { - $0.background(isDark ? Color.tint(0.12) : Color.shade(0.06)) - .clipShape(Circle()) - } .background( Circle() - .fill(Color(designSystemColor: .background)) + .fill(backgroundColor) ) .overlay { configuration.label - .foregroundColor(isDark ? .tint(0.6) : .shade(0.6)) + .foregroundColor(Color(designSystemColor: .iconsSecondary)) .frame(width: 16, height: 16) } diff --git a/DuckDuckGo/ViewExtension.swift b/DuckDuckGo/ViewExtension.swift index ca1afc56cc..3c00ddf314 100644 --- a/DuckDuckGo/ViewExtension.swift +++ b/DuckDuckGo/ViewExtension.swift @@ -19,21 +19,6 @@ import SwiftUI -extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } -} - extension View { /// Disables scroll if available for current system version @available(iOS, deprecated: 16.0, renamed: "scrollDisabled") diff --git a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift index 77776002ae..cea616b398 100644 --- a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift +++ b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift @@ -18,13 +18,13 @@ // import XCTest +import Combine import Bookmarks -import BrowserServicesKit @testable import DuckDuckGo final class NewTabPageFavoritesModelTests: XCTestCase { - private let favoritesListInteracting = MockFavoritesListInteracting() - + private let favoriteDataSource = MockNewTabPageFavoriteDataSource() + override func tearDown() { PixelFiringMock.tearDown() } @@ -59,23 +59,23 @@ final class NewTabPageFavoritesModelTests: XCTestCase { } func testFiresPixelOnFavoriteDeleted() { - let bookmark = createStubBookmark() - favoritesListInteracting.favorites = [bookmark] + let favorite = Favorite.stub() + favoriteDataSource.favorites = [favorite] let sut = createSUT() - sut.deleteFavorite(Favorite(id: bookmark.uuid!, title: "", domain: "")) + sut.deleteFavorite(favorite) XCTAssertEqual(PixelFiringMock.lastPixel, .homeScreenDeleteFavorite) } func testFiresPixelOnFavoriteEdited() { - let bookmark = createStubBookmark() - favoritesListInteracting.favorites = [bookmark] + let favorite = Favorite.stub() + favoriteDataSource.favorites = [favorite] let sut = createSUT() - sut.editFavorite(Favorite(id: bookmark.uuid!, title: "", domain: "")) + sut.editFavorite(favorite) XCTAssertEqual(PixelFiringMock.lastPixel, .homeScreenEditFavorite) } @@ -97,17 +97,35 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertEqual(PixelFiringMock.lastPixel, .newTabPageFavoritesInfoTooltip) } + private func createSUT() -> FavoritesDefaultViewModel { + FavoritesDefaultViewModel(favoriteDataSource: favoriteDataSource, + faviconLoader: FavoritesFaviconLoader(), + pixelFiring: PixelFiringMock.self, + dailyPixelFiring: PixelFiringMock.self) + } +} + +private final class MockNewTabPageFavoriteDataSource: NewTabPageFavoriteDataSource { + var externalUpdates: AnyPublisher = Empty().eraseToAnyPublisher() + var favorites: [DuckDuckGo.Favorite] = [] + + func moveFavorite(_ favorite: DuckDuckGo.Favorite, fromIndex: Int, toIndex: Int) { } + func favorite(at index: Int) throws -> DuckDuckGo.Favorite? { nil } + func removeFavorite(_ favorite: DuckDuckGo.Favorite) { } + func bookmarkEntity(for favorite: DuckDuckGo.Favorite) -> Bookmarks.BookmarkEntity? { + createStubBookmark() + } + private func createStubBookmark() -> BookmarkEntity { let bookmarksDB = MockBookmarksDatabase.make() let context = bookmarksDB.makeContext(concurrencyType: .mainQueueConcurrencyType) let root = BookmarkUtils.fetchRootFolder(context)! return BookmarkEntity.makeBookmark(title: "foo", url: "", parent: root, context: context) } +} - private func createSUT() -> FavoritesDefaultModel { - FavoritesDefaultModel(interactionModel: favoritesListInteracting, - faviconLoader: FavoritesFaviconLoader(), - pixelFiring: PixelFiringMock.self, - dailyPixelFiring: PixelFiringMock.self) +private extension Favorite { + static func stub() -> Favorite { + Favorite(id: UUID().uuidString, title: "foo", domain: "bar") } } diff --git a/DuckDuckGoTests/NewTabPageModelTests.swift b/DuckDuckGoTests/NewTabPageViewModelTests.swift similarity index 79% rename from DuckDuckGoTests/NewTabPageModelTests.swift rename to DuckDuckGoTests/NewTabPageViewModelTests.swift index 7025fed9c1..4c42df78c9 100644 --- a/DuckDuckGoTests/NewTabPageModelTests.swift +++ b/DuckDuckGoTests/NewTabPageViewModelTests.swift @@ -1,5 +1,5 @@ // -// NewTabPageModelTests.swift +// NewTabPageViewModelTests.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -20,7 +20,7 @@ import XCTest @testable import DuckDuckGo -final class NewTabPageModelTests: XCTestCase { +final class NewTabPageViewModelTests: XCTestCase { let introDataStorage = NewTabPageIntroDataStoringMock() @@ -29,21 +29,21 @@ final class NewTabPageModelTests: XCTestCase { } func testDoesNotShowIntroIfSettingUndefined() { - let sut = NewTabPageModel(introDataStorage: introDataStorage) + let sut = NewTabPageViewModel(introDataStorage: introDataStorage) XCTAssertFalse(sut.isIntroMessageVisible) } func testShowsIntroMessage() { introDataStorage.newTabPageIntroMessageEnabled = true - let sut = NewTabPageModel(introDataStorage: introDataStorage) + let sut = NewTabPageViewModel(introDataStorage: introDataStorage) XCTAssertTrue(sut.isIntroMessageVisible) } func testDisablesIntroMessageWhenDismissed() { introDataStorage.newTabPageIntroMessageEnabled = true - let sut = NewTabPageModel(introDataStorage: introDataStorage) + let sut = NewTabPageViewModel(introDataStorage: introDataStorage) sut.dismissIntroMessage() @@ -53,7 +53,7 @@ final class NewTabPageModelTests: XCTestCase { func testDisablesIntroMessageAfterMultipleImpressions() { introDataStorage.newTabPageIntroMessageEnabled = true - let sut = NewTabPageModel(introDataStorage: introDataStorage) + let sut = NewTabPageViewModel(introDataStorage: introDataStorage) for _ in 1...3 { sut.introMessageDisplayed() @@ -64,7 +64,7 @@ final class NewTabPageModelTests: XCTestCase { } func testFiresPixelWhenIntroMessageDismissed() { - let sut = NewTabPageModel(pixelFiring: PixelFiringMock.self) + let sut = NewTabPageViewModel(pixelFiring: PixelFiringMock.self) sut.dismissIntroMessage() @@ -72,7 +72,7 @@ final class NewTabPageModelTests: XCTestCase { } func testFiresPixelWhenIntroMessageDisplayed() { - let sut = NewTabPageModel(pixelFiring: PixelFiringMock.self) + let sut = NewTabPageViewModel(pixelFiring: PixelFiringMock.self) sut.introMessageDisplayed() @@ -80,7 +80,7 @@ final class NewTabPageModelTests: XCTestCase { } func testFiresPixelOnNewTabPageCustomize() { - let sut = NewTabPageModel(pixelFiring: PixelFiringMock.self) + let sut = NewTabPageViewModel(pixelFiring: PixelFiringMock.self) sut.customizeNewTabPage() diff --git a/LocalPackages/DuckUI/Sources/DuckUI/Button.swift b/LocalPackages/DuckUI/Sources/DuckUI/Button.swift index d28599404c..74a825ba31 100644 --- a/LocalPackages/DuckUI/Sources/DuckUI/Button.swift +++ b/LocalPackages/DuckUI/Sources/DuckUI/Button.swift @@ -99,11 +99,13 @@ public struct SecondaryFillButtonStyle: ButtonStyle { let disabled: Bool let compact: Bool let fullWidth: Bool + let isFreeform: Bool - public init(disabled: Bool = false, compact: Bool = false, fullWidth: Bool = true) { + public init(disabled: Bool = false, compact: Bool = false, fullWidth: Bool = true, isFreeform: Bool = false) { self.disabled = disabled self.compact = compact self.fullWidth = fullWidth + self.isFreeform = isFreeform } public func makeBody(configuration: Configuration) -> some View { @@ -122,9 +124,12 @@ public struct SecondaryFillButtonStyle: ButtonStyle { .lineLimit(nil) .font(Font(UIFont.boldAppFont(ofSize: Consts.fontSize))) .foregroundColor(configuration.isPressed ? defaultForegroundColor : foregroundColor) - .padding(.vertical) - .padding(.horizontal, fullWidth ? nil : 24) - .frame(minWidth: 0, maxWidth: fullWidth ? .infinity : nil, maxHeight: compact ? Consts.height - 10 : Consts.height) + .if(!isFreeform) { view in + view + .padding(.vertical) + .padding(.horizontal, fullWidth ? nil : 24) + .frame(minWidth: 0, maxWidth: fullWidth ? .infinity : nil, maxHeight: compact ? Consts.height - 10 : Consts.height) + } .background(configuration.isPressed ? pressedBackgroundColor : backgroundColor) .cornerRadius(Consts.cornerRadius) } diff --git a/LocalPackages/DuckUI/Sources/DuckUI/ViewExtensions/ViewModifyExtension.swift b/LocalPackages/DuckUI/Sources/DuckUI/ViewExtensions/ViewModifyExtension.swift new file mode 100644 index 0000000000..9d2a37baa0 --- /dev/null +++ b/LocalPackages/DuckUI/Sources/DuckUI/ViewExtensions/ViewModifyExtension.swift @@ -0,0 +1,35 @@ +// +// ViewModifyExtension.swift +// DuckDuckGo +// +// Copyright © 2023 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 + +public extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index c3fd0e2ff4..1d00f55394 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -33,7 +33,7 @@ let package = Package( ], dependencies: [ .package(path: "../DuckUI"), - .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.2.0"), + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ diff --git a/LocalPackages/Waitlist/Package.swift b/LocalPackages/Waitlist/Package.swift index 72ad52d8b2..13b5954238 100644 --- a/LocalPackages/Waitlist/Package.swift +++ b/LocalPackages/Waitlist/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: ["Waitlist", "WaitlistMocks"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.2.0"), + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ From 8fa946ebd9a8f64701f7a0ccf99ab1a133d29620 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 23 Sep 2024 12:45:54 +0200 Subject: [PATCH 22/46] Upgrade to Punycode 3.0.0 (#3370) Task/Issue URL: https://app.asana.com/0/1200194497630846/1208348353779307/f Description: This change upgrades Punycode to the latest release. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 272857cd89..5ed067951d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10939,7 +10939,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.0.0; + version = 198.0.1; }; }; 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 fd1bb75388..4febefbd6a 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" : "6e1520bd83bbcc269b0d561c51fc92b81fe6d93b", - "version" : "198.0.0" + "revision" : "cc3629fa16880e410e588a27a6b2426dcc140009", + "version" : "198.0.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "9f3717b3913a12956f1386fe7b657f68545fba83", - "version" : "6.15.0" + "revision" : "4f0d109f13beec7e8beaf0bd5c0e2c1d528677f8", + "version" : "6.16.0" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gumob/PunycodeSwift.git", "state" : { - "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", - "version" : "2.1.0" + "revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5", + "version" : "3.0.0" } }, { From ff8282af9aad175554ad37b4786a175c1218c93d Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 23 Sep 2024 14:48:49 +0200 Subject: [PATCH 23/46] Localize autofill security concern mitigation copy (#3379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1208341864628593/f **Description**: Localizations for https://app.asana.com/0/1199230911884351/1208316169332403 **Steps to test this PR**: 1. Switch to another language and step through the screens from [here](https://www.figma.com/design/wAWx1a0mAooj6sDCmoTFbS/Password-Manager-security?node-id=192-10952&node-type=frame&t=ZmmHbkxMmNCrEAWa-0) to check they've been translated **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) --- DuckDuckGo/bg.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/cs.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/da.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/de.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/el.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/es.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/et.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/fi.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/fr.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/hr.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/hu.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/it.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/lt.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/lv.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/nb.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/nl.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/pl.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/pt.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/ro.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/ru.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/sk.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/sl.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/sv.lproj/Localizable.strings | 17 +++++++++++++---- DuckDuckGo/tr.lproj/Localizable.strings | 17 +++++++++++++---- 24 files changed, 312 insertions(+), 96 deletions(-) diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index 35bb6aa4a9..08cabf8dbb 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Редактиране"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Забрани"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Преглед"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Импортиране на пароли"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Паролите от други браузъри или приложения могат да бъдат импортирани чрез версията за работен плот на браузъра DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Можете да импортирате запазени пароли от друг браузър в DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Все още няма запазени пароли"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Предложения"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Никога не питай за този сайт"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Passwords & Autofill съхранява паролите по сигурен начин на Вашето устройство."; - /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Запазване на тази парола?"; +/* Message displayed on modal asking for the user to save the login for the first time */ +"autofill.save-login.security.message" = "С DuckDuckGo Passwords & Autofill можете да съхранявате паролата по сигурен начин на устройството."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Запазване на паролата?"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index 72f786bc8d..2ee7508f47 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Upravit"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Vypnout"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Zobrazit"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importovat hesla"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Hesla z jiných prohlížečů nebo aplikací se dají importovat pomocí verze prohlížeče DuckDuckGo pro počítače."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Uložená hesla můžeš importovat z jiného prohlížeče do DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Zatím nemáš uložená žádná hesla"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Navrhované"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Na téhle stránce už se neptat"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Funkce ukládání a automatického vyplňování hesel DuckDuckGo bezpečně ukládá hesla ve tvém zařízení."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Uložit heslo?"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index cf77933d37..e15bd57e10 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Rediger"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Deaktiver"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Se"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importer adgangskoder"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Adgangskoder fra andre browsere eller apps kan importeres ved hjælp af computerversionen af DuckDuckGo-browseren."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Du kan importere gemte adgangskoder fra en anden browser til DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Ingen adgangskoder gemt endnu"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Foreslået"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Spørg aldrig på dette websted"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo adgangskoder og automatisk udfyldning gemmer adgangskoder sikkert på din enhed."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Gem adgangskode?"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 6459a2bbce..9a403f38c1 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Bearbeiten"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Deaktivieren"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Ansehen"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Passwörter importieren"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Passwörter aus anderen Browsern oder Apps können mit der Desktop-Version des DuckDuckGo-Browsers importiert werden."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Du kannst gespeicherte Passwörter aus einem anderen Browser in DuckDuckGo importieren."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Noch keine Passwörter gespeichert"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Vorgeschlagen"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Für diese Website niemals fragen"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Passwörter & Autovervollständigen speichert Passwörter sicher auf deinem Gerät."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Passwort speichern?"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 4d4724f848..593f65c6b6 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Επεξεργασία"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Απενεργοποίηση"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Προβολή"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Εισαγωγή κωδικών πρόσβασης"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Οι κωδικοί πρόσβασης από άλλα προγράμματα περιήγησης ή εφαρμογές μπορούν να εισαχθούν με χρήση της έκδοσης του προγράμματος περιήγησης DuckDuckGo για υπολογιστές."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Μπορείτε να εισαγάγετε αποθηκευμένους κωδικούς πρόσβασης από άλλο πρόγραμμα περιήγησης στο DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Δεν έχουν αποθηκευτεί ακόμα κωδικοί πρόσβασης"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Προτεινόμενο"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Μην ζητάτε ποτέ αυτόν τον ιστότοπο"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Η λειτουργία DuckDuckGo κωδικοί πρόσβασης και αυτόματη συμπλήρωση αποθηκεύει τους κωδικούς πρόσβασης με ασφάλεια στη συσκευή σας."; - /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Αποθήκευση αυτού του κωδικού πρόσβασης;"; +/* Message displayed on modal asking for the user to save the login for the first time */ +"autofill.save-login.security.message" = "Αποθηκεύστε με ασφάλεια τον κωδικό πρόσβασής σας στη συσκευή με τη λειτουργία DuckDuckGo κωδικοί πρόσβασης και αυτόματη συμπλήρωση."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Αποθήκευση κωδικού πρόσβασης;"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 0751644415..397ff70979 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Editar"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Deshabilitar"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Ver"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importar contraseñas"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Las contraseñas de otros navegadores o aplicaciones se pueden importar utilizando la versión de escritorio del navegador DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Puedes importar contraseñas guardadas de otro navegador a DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Aún no hay contraseñas guardadas"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Sugerencias"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "No preguntar nunca para esta página"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Contraseñas y Autocompletar de DuckDuckGo almacena las contraseñas de forma segura en tu dispositivo."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "¿Guardar contraseña?"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index 8a72451e11..e016626981 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Redigeeri"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Keela"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Kuva"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Impordi paroolid"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Teiste brauserite või rakenduste paroole saab importida DuckDuckGo brauseri töölauaversiooni abil."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Salvestatud paroole saad importida teisest brauserist DuckDuckGo-sse."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Paroole ei ole veel salvestatud"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Soovitatud"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Ära selle saidi kohta rohkem küsi"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo paroolid ja automaatne täitmine salvestab paroole turvaliselt sinu seadmes."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Kas salvestada parool?"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 85ee101acb..f56d2050e4 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Muokkaa"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Poista Käytöstä"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Näytä"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Tuo salasanat"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Muiden selainten tai sovellusten salasanat voidaan tuoda DuckDuckGo-selaimen tietokoneversiolla."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Voit tuoda tallennetut salasanat toisesta selaimesta DuckDuckGohon."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Salasanoja ei ole vielä tallennettu"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Ehdotettu"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Älä kysy enää tällä sivustolla"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGon salasanat ja automaattinen täyttö tallentaa salasanat turvallisesti laitteellesi."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Tallennetaanko salasana?"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index 6c9382eb6a..c0935e7ba1 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Modifier"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Désactiver"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Voir"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importer les mots de passe"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Les mots de passe d'autres navigateurs ou applications peuvent être importés à l'aide de la version de bureau du navigateur DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Vous pouvez importer les mots de passe enregistrés d'un autre navigateur dans DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Aucun mot de passe n'a été enregistré"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Suggéré"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Ne jamais demander pour ce site"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Dans DuckDuckGo, « Mots de passe et saisie automatique » stocke les mots de passe en toute sécurité sur votre appareil."; - /* 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 ?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Enregistrer le mot de passe ?"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 8e72639a3d..563b34bdc0 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Uredi"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Onemogući"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Pregled"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Uvezi lozinke"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Lozinke iz drugih preglednika ili aplikacija mogu se uvesti pomoću desktop verzije preglednika DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Možeš uvesti spremljene lozinke iz drugog preglednika u DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Još nema spremljenih lozinki"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Predloženo"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nikada ne traži za ovu web lokaciju"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo funkcija Passwords & Autofill sigurno pohranjuje lozinke na tvom uređaju."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Želiš li spremiti lozinku?"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index ea6c868cba..3399b62a5b 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Szerkesztés"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Letilt"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Megtekintés"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Jelszavak importálása"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Más böngészőkből vagy alkalmazásokból származó jelszavak a DuckDuckGo böngésző asztali verziójával importálhatók."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Importálhatod a mentett jelszavaidat egy másik böngészőből a DuckDuckGo böngészőjébe."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Még nincsenek mentett jelszavak"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Javasolt"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Soha ne kérdezzen rá ennél a webhelynél"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "A DuckDuckGóban elérhető Jelszavak és automatikus kitöltés funkció biztonságosan tárolja a jelszavakat az eszközön."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Mented a jelszót?"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index 4e80e35737..4655aa644b 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Modifica"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Disabilita"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Visualizza"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importa password"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Le password di altri browser o app possono essere importate utilizzando la versione desktop del browser DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Puoi importare le password salvate da un altro browser in DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Nessuna password ancora salvata"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Suggerimenti"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Non chiedere mai per questo sito"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Password e compilazione automatica archivia le password in modo sicuro sul tuo dispositivo."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Salvare password?"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index 193dfdf75a..bffebb0cd7 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Redaguoti"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Išjungti"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Peržiūrėti"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importuoti slaptažodžius"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Slaptažodžius iš kitų naršyklių ar programų galima importuoti naudojant „DuckDuckGo“ naršyklės versiją kompiuteriui."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Galite importuoti išsaugotus slaptažodžius iš kitos naršyklės į „DuckDuckGo“."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Dar nėra išsaugotų slaptažodžių"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Siūloma"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Niekada neklauskite šioje svetainėje"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "„DuckDuckGo“ priemonė „Slaptažodžiai ir automatinis užpildymas“ saugiai saugo slaptažodžius jūsų įrenginyje."; - /* 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į?"; +/* 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į."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Išsaugoti slaptažodį?"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 9fde6bb665..91f43d110d 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Rediģēt"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Atslēgt"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Skatīt"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importēt paroles"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Paroles no citiem pārlūkiem vai lietotnēm var importēt, izmantojot DuckDuckGo pārlūka galddatora versiju."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Tu vari DuckDuckGo importēt saglabātās paroles no citas pārlūkprogrammas."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Vēl nav saglabāta neviena parole"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Ieteikts"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nekad nejautāt par šo vietni"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo funkcija Paroles un automātiskā aizpildīšana droši glabā paroles tavā ierīcē."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Saglabāt paroli?"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index 320d6c30a4..907ef03667 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Rediger"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Slå av"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Vis"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importer passord"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Passord fra andre nettlesere eller apper kan importeres med skrivebordsversjonen av DuckDuckGo-nettleseren."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Du kan importere lagrede passord fra en annen nettleser til DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Ingen passord er lagret ennå"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Forslag"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Aldri spør for dette nettstedet"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo-passord og -autofyll lagrer passord trygt på enheten din."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Vil du lagre passordet?"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index c4e0a07bb0..4078831a30 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Bewerken"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Uitschakelen"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Weergeven"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Wachtwoorden importeren"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Wachtwoorden van andere browsers of apps kunnen worden geïmporteerd met de desktopversie van de DuckDuckGo-browser."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Je kunt opgeslagen wachtwoorden uit een andere browser importeren in DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Nog geen wachtwoorden opgeslagen"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Aanbevolen"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nooit vragen voor deze site"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Wachtwoorden en Automatisch aanvullen slaat wachtwoorden veilig op je apparaat op."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Wachtwoord opslaan?"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index aa0e54a7ef..806d45b3b2 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Edytuj"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Wyłącz"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Wyświetl"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importuj hasła"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Hasła z innych przeglądarek lub aplikacji można zaimportować za pomocą komputerowej wersji przeglądarki DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Możesz importować zapisane hasła z innej przeglądarki do DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Nie zapisano jeszcze żadnych haseł"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Sugerowane"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nigdy nie pytaj o tę witrynę"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Funkcja Hasła i autouzupełnianie DuckDuckGo bezpiecznie przechowuje hasła na urządzeniu."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Zapisać hasło?"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index 26a07512de..d212dbd9a3 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Editar"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Desabilitar (fora)"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Visualizar"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importar palavras-passe"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "As palavras-passe de outros navegadores ou aplicações podem ser importadas com a versão para computadores do navegador DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Podes importar palavras-passe guardadas de outro navegador para o DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Ainda não há palavras-passe guardadas"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Sugerido"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nunca pedir para este site"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "A funcionalidade Palavras-passe e preenchimento automático do DuckDuckGo armazena as palavras-passe com segurança no teu dispositivo."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Guardar palavra-passe?"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index a5b21cd810..6a4093c3aa 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Editați"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Dezactivează"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Vezi"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importă parolele"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Parolele din alte browsere sau aplicații pot fi importate folosind versiunea pentru desktop a browserului DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Poți importa parolele salvate dintr-un alt browser în DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Nici o parolă nu a fost salvată încă"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Sugerat"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nu solicita niciodată pentru acest site"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Parole și completare automată de la DuckDuckGo stochează parolele în siguranță pe dispozitivul tău."; - /* 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ă?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Salvezi parola?"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 24e34238ce..57442b594e 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Редактировать"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Выключить"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Просмотр"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Импорт паролей"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Пароли из других браузеров и приложений можно импортировать с помощью настольной версии DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "В DuckDuckGo можно импортировать сохраненные пароли из другого браузера."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Сохраненных паролей пока нет"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Рекомендации"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Больше не спрашивать на этом сайте"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Функция «Пароли и автозаполнение» в составе DuckDuckGo надежно хранит пароли на вашем устройстве."; - /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Сохранить пароль?"; +/* Message displayed on modal asking for the user to save the login for the first time */ +"autofill.save-login.security.message" = "Вы можете сохранить этот пароль на устройстве под надежной защитой функции «Пароли и автозаполнение» от DuckDuckGo."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Сохранить пароль?"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index ab8ce098ee..5811b9f9d7 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Upraviť"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Zakázať"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Zobraziť"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importovať heslá"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Heslá z iných prehliadačov alebo aplikácií možno importovať pomocou verzie prehliadača DuckDuckGo pre počítače."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Uložené heslá môžete importovať z iného prehliadača do DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Zatiaľ nie sú uložené žiadne heslá"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Navrhované"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nikdy sa nepýtať na túto stránku"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Passwords & Autofill bezpečne ukladá heslá vo vašom zariadení."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Uložiť heslo?"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 1606d05a14..49331cce74 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Uredi"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Onemogoči"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Oglejte si"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Uvozi gesla"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Gesla iz drugih brskalnikov ali aplikacij lahko uvozite z namizno različico brskalnika DuckDuckGo."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Shranjena gesla lahko uvozite iz drugega brskalnika v DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Nobeno geslo še ni shranjeno"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Predlagano"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Nikoli ne vprašaj za to spletno mesto"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Funkcija DuckDuckGo Passwords & Autofill varno shranjuje gesla v vaši napravi."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Želite shraniti geslo?"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index d90d21d646..39074f10d3 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Redigera"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Inaktivera"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Visa"; @@ -419,7 +425,7 @@ "autofill.logins.empty-view.button.title" = "Importera lösenord"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Lösenord från andra webbläsare eller appar kan importeras med hjälp av datorversionen av DuckDuckGo-webbläsaren."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "Du kan importera sparade lösenord från andra webbläsare till DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "Inga lösenord sparade ännu"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Förslag"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Fråga aldrig för den här webbplatsen"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Lösenord och autofyll lagrar lösenord på ett säkert sätt på din enhet."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Spara lösenord?"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index 28cfbe8e97..348ada4d1f 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ " = "%@ "; + /* Buton label for Edit action */ "action.generic.edit" = "Düzenle"; @@ -325,6 +328,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Devre dışı bırak"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "Görüntüle"; @@ -419,7 +425,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" = "Diğer tarayıcılardan veya uygulamalardan şifreler, DuckDuckGo tarayıcısının masaüstü sürümü kullanılarak içe aktarılabilir."; +"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"; @@ -463,6 +469,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. 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/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Önerilen"; @@ -581,12 +590,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Bu Site için Hiçbir Zaman Sorma"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Şifreleri ve Otomatik Doldurma, parolaları cihazınızda güvenli bir şekilde saklar."; - /* 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?"; +/* 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."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Şifre kaydedilsin mi?"; From d57df6de64c3c76b9c226df4be5e7d1a4725cf6e Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:37:58 -0400 Subject: [PATCH 24/46] Release 7.139.0-0 (#3383) --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 99 ++++++++++++++----- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++------ DuckDuckGo/Settings.bundle/Root.plist | 2 +- 5 files changed, 109 insertions(+), 54 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index c1d3187e3c..31ce09d561 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.138.0 +MARKETING_VERSION = 7.139.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index a029b30de7..e74c5be512 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"24a5ebbe8d8bb826ca87eacd9e26c281\"" - public static let embeddedDataSHA = "9c4301232c8c0429f4148e5d23c4ec065bffffdf6be14dfdb838ffd9f46b9bb5" + public static let embeddedDataETag = "\"aa6acaab3804053c652b64a3568cee2b\"" + public static let embeddedDataSHA = "d56a1b7ff72713333d2d17e6825a6ab8f14d3f87b4b77c2406d74840d393960b" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index 9bfa01120a..609728c835 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1726491752161, + "version": 1727101913728, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -129,6 +129,11 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "autocompleteTabs": { + "state": "enabled", + "exceptions": [], + "hash": "697382e31649d84b01166f1dc6f790d6" + }, "autoconsent": { "exceptions": [ { @@ -341,6 +346,15 @@ { "domain": "lehmannaudio.com" }, + { + "domain": "rivergrandrapids.com" + }, + { + "domain": "97x.com" + }, + { + "domain": "speedweek.com" + }, { "domain": "marvel.com" }, @@ -388,7 +402,7 @@ } } }, - "hash": "a235f7ab6a5a3632380570bbc5113d51" + "hash": "ee8caeb715c81bf398024ed686bc3f5c" }, "autofillBreakageReporter": { "state": "enabled", @@ -478,6 +492,23 @@ }, "hash": "28d4af98382248e184c4315bd49f4222" }, + "backgroundAgentPixelTest": { + "state": "enabled", + "exceptions": [], + "features": { + "pixelTest": { + "state": "enabled", + "rollout": { + "steps": [ + { + "percent": 10 + } + ] + } + } + }, + "hash": "75ff21cf9a4181783c06965edd6fe746" + }, "bookmarks": { "state": "enabled", "exceptions": [], @@ -522,6 +553,11 @@ "state": "enabled", "hash": "52857469413a66e8b0c7b00de5589162" }, + "changeOmnibarPosition": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "clickToLoad": { "exceptions": [ { @@ -1841,6 +1877,10 @@ "selector": "[aria-label='advertisement']", "type": "hide-empty" }, + { + "selector": ".inline-ad", + "type": "closest-empty" + }, { "selector": ".ads__inline", "type": "closest-empty" @@ -2024,6 +2064,10 @@ { "selector": ".m-in-content-ad-row", "type": "hide-empty" + }, + { + "selector": ".clsy-c-advsection", + "type": "hide-empty" } ], "styleTagExceptions": [ @@ -2099,7 +2143,8 @@ "continue reading the main story", "this advertisement has not loaded yet, but your article continues below.", "story continues below\nthis advertisement has not loaded yet, but your article continues below.", - "upgrade to flickr pro to hide these ads" + "upgrade to flickr pro to hide these ads", + "video of the day" ], "domains": [ { @@ -4276,6 +4321,19 @@ } ] }, + { + "domain": "techwalla.com", + "rules": [ + { + "selector": ".component-header-sticky-ad", + "type": "closest-empty" + }, + { + "selector": ".component-article-section-votd-wrapper", + "type": "hide-empty" + } + ] + }, { "domain": "theatlantic.com", "rules": [ @@ -4831,7 +4889,7 @@ ] }, "state": "enabled", - "hash": "ad92e313ec7c6f14ad7d142d04bccf72" + "hash": "0882839182c425cbc9f98b8329036254" }, "exceptionHandler": { "exceptions": [ @@ -5507,6 +5565,11 @@ "exceptions": [], "hash": "429cea8d27316dc62af04159ec7c42b5" }, + "loadingBarExp": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "marketplaceAdPostback": { "state": "enabled", "exceptions": [], @@ -5709,9 +5772,13 @@ }, "allowPurchase": { "state": "enabled" + }, + "useUnifiedFeedback": { + "state": "enabled", + "minSupportedVersion": "7.136.0" } }, - "hash": "5ba91c7416564247c49dae3e80bd5a3c" + "hash": "c2a2ca6f3ae1b8ddb4f3fa011cb3e227" }, "privacyProtectionsPopup": { "state": "disabled", @@ -6038,7 +6105,7 @@ ] }, { - "rule": "static.adsafeprotected.com/skeleton.gif", + "rule": "static.adsafeprotected.com/skeleton", "domains": [ "" ] @@ -7273,20 +7340,7 @@ { "rule": "pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", "domains": [ - "air-journal.fr", - "arcadepunks.com", - "daotranslate.com", - "drakescans.com", - "duden.de", - "edealinfo.com", - "freetubetv.net", - "hscprojects.com", - "kits4beats.com", - "magicgameworld.com", - "ncaa.com", - "rocketnews24.com", - "youmath.it", - "zefoy.com" + "" ] }, { @@ -8742,7 +8796,8 @@ "rule": "sundaysky.com", "domains": [ "bankofamerica.com", - "idnotify.com" + "idnotify.com", + "united.com" ] } ] @@ -9227,7 +9282,7 @@ "domain": "capitalone.com" } ], - "hash": "7c26c9953069fedb8fe6532c86cf5572" + "hash": "d41dab46e3e6b98fa00b3f09d4982a14" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5ed067951d..5cd8288d04 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9154,7 +9154,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9191,7 +9191,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9281,7 +9281,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9308,7 +9308,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9457,7 +9457,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9482,7 +9482,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9551,7 +9551,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9585,7 +9585,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9618,7 +9618,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9648,7 +9648,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9958,7 +9958,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9989,7 +9989,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10017,7 +10017,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10050,7 +10050,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10080,7 +10080,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10113,11 +10113,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10350,7 +10350,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10377,7 +10377,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10409,7 +10409,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10446,7 +10446,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10481,7 +10481,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10516,11 +10516,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10693,11 +10693,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10726,10 +10726,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 4aebc33710..ae544da517 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.138.0 + 7.139.0 Key version Title From d0f5dfd195700f43db2ca0bc937e521ed7220ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 24 Sep 2024 11:41:43 +0200 Subject: [PATCH 25/46] Trigger add-task-to-project when PR marked ready for review (#3376) Task/Issue URL: https://app.asana.com/0/414235014887631/1207931637636063/f Tech Design URL: CC: **Description**: Trigger add-task-to-project when PR marked ready for review. Counterpart of https://github.com/duckduckgo/macos-browser/pull/3273. **Steps to test this PR**: * Create a copy of this branch and open a draft PR. * Mark as ready for review and check if the "Add Task to App Board Project" action is successful. **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) --- .github/workflows/pr-task-url.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-task-url.yml b/.github/workflows/pr-task-url.yml index dad73e8220..7f130fe540 100644 --- a/.github/workflows/pr-task-url.yml +++ b/.github/workflows/pr-task-url.yml @@ -2,7 +2,7 @@ name: Asana PR Task URL on: pull_request: - types: [opened, edited, closed, synchronize, review_requested] + types: [opened, edited, closed, synchronize, review_requested, ready_for_review] jobs: @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} + outputs: task_id: ${{ steps.get-task-id.outputs.task_id }} task_in_project: ${{ steps.check-board-membership.outputs.task_in_project }} @@ -47,7 +49,7 @@ jobs: - name: Add Task to the App Board Project id: add-task-to-project - if: ${{ github.event.action == 'opened' && steps.check-board-membership.outputs.task_in_project == '0' }} + if: ${{ (github.event.action == 'opened' || github.event.action == 'ready_for_review') && steps.check-board-membership.outputs.task_in_project == '0' }} env: ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} ASANA_PROJECT_ID: ${{ vars.IOS_APP_BOARD_ASANA_PROJECT_ID }} From 2d9ec00309ad2e1a4adb19680f9faea76d63a6ef Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 24 Sep 2024 11:56:12 +0100 Subject: [PATCH 26/46] fix return user segmentation (#3387) Task/Issue URL: https://app.asana.com/0/414709148257752/1208380046395184/f Tech Design URL: CC: **Description**: Fix usage segmentation for return users. **Steps to test this PR**: 1. Reset the simulator `xcrun simctl erase all` 2. Add the following lines to the start of the `load` func of `StatisticsLoader`: statisticsStore.variant = "ru" statisticsStore.atb = "v446-1" statisticsStore.appRetentionAtb = "v447-1" statisticsStore.searchRetentionAtb = "v447-2" 4. Run the app and check for a pixel like this: Pixel fired m.retention.segments ["count_as_mau_n": "tttt", "new_set_atb": "v448-7", "count_as_wau": "true", "segments_today": "first_month,reactivated_wau,reinstaller", "activity_type": "app_use"] 5. Ensure that `reinstaller` and `"activity_type": "app_use"` are in the parameters as above 6. Get through onboarding and do a search 7. Check for a similar pixel ensuring that `reinstaller` and `"activity_type": "search"` are in the parameters **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- Core/ReturnUserMeasurement.swift | 4 +++ Core/StatisticsLoader.swift | 7 +++--- DuckDuckGoTests/StatisticsLoaderTests.swift | 28 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Core/ReturnUserMeasurement.swift b/Core/ReturnUserMeasurement.swift index ba33d836ab..67b619677b 100644 --- a/Core/ReturnUserMeasurement.swift +++ b/Core/ReturnUserMeasurement.swift @@ -20,8 +20,12 @@ import Foundation import BrowserServicesKit +/// This is only intended to be used during the install (first run after downloading from the app store). protocol ReturnUserMeasurement { + /// Based on the value in the keychain, so if you use this after the install process it will return true. + /// If you really want to know if the user is "returning" then look at the variant in the `StatisticsStore` + /// which will be set to `ru`. var isReturningUser: Bool { get } func installCompletedWithATB(_ atb: Atb) func updateStoredATB(_ atb: Atb) diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index dd8a624685..38255c9097 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -155,9 +155,10 @@ public class StatisticsLoader { private func processUsageSegmentation(atb: Atb?, activityType: UsageActivityType) { guard let installAtbValue = statisticsStore.atb else { return } - let installAtb = Atb(version: installAtbValue, updateVersion: nil) - let actualAtb = atb ?? installAtb - self.usageSegmentation.processATB(actualAtb, withInstallAtb: installAtb, andActivityType: activityType) + let installAtb = Atb(version: installAtbValue + (statisticsStore.variant ?? ""), updateVersion: nil) + let usageAtb = atb ?? installAtb + + self.usageSegmentation.processATB(usageAtb, withInstallAtb: installAtb, andActivityType: activityType) } private func updateUsageSegmentationWithAtb(_ atb: Atb, activityType: UsageActivityType) { diff --git a/DuckDuckGoTests/StatisticsLoaderTests.swift b/DuckDuckGoTests/StatisticsLoaderTests.swift index 063425f707..64dca2b495 100644 --- a/DuckDuckGoTests/StatisticsLoaderTests.swift +++ b/DuckDuckGoTests/StatisticsLoaderTests.swift @@ -42,6 +42,34 @@ class StatisticsLoaderTests: XCTestCase { super.tearDown() } + func testWhenAppRefreshHappensButNotInstalledAndReturningUser_ThenRetentionSegmentationNotified() { + mockStatisticsStore.variant = "ru" + mockStatisticsStore.atb = "v101-1" + + loadSuccessfulExiStub() + + let testExpectation = expectation(description: "refresh complete") + testee.refreshAppRetentionAtb { + testExpectation.fulfill() + } + wait(for: [testExpectation], timeout: 5.0) + XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser) + } + + func testWhenReturnUser_ThenSegmentationIncludesCorrectVariant() { + mockStatisticsStore.variant = "ru" + mockStatisticsStore.atb = "v101-1" + mockStatisticsStore.searchRetentionAtb = "v101-2" + loadSuccessfulAtbStub() + + let testExpectation = expectation(description: "refresh complete") + testee.refreshSearchRetentionAtb { + testExpectation.fulfill() + } + wait(for: [testExpectation], timeout: 5.0) + XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser) + } + func testWhenSearchRefreshHappensButNotInstalled_ThenRetentionSegmentationNotified() { loadSuccessfulExiStub() From 8a8a8df816c754f40ae0169e0056fb0f3e80d83f Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 24 Sep 2024 12:12:04 +0100 Subject: [PATCH 27/46] Release 7.138.1-0 (#3388) --- Configuration/Version.xcconfig | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++++++++++-------------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index c1d3187e3c..cff94a52b6 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.138.0 +MARKETING_VERSION = 7.138.1 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2686c74017..18e8d0eb00 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9100,7 +9100,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9137,7 +9137,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9227,7 +9227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9254,7 +9254,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9403,7 +9403,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9428,7 +9428,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9497,7 +9497,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9531,7 +9531,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9564,7 +9564,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9594,7 +9594,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9904,7 +9904,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9935,7 +9935,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9963,7 +9963,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9996,7 +9996,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10026,7 +10026,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10059,11 +10059,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10296,7 +10296,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10323,7 +10323,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10355,7 +10355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10392,7 +10392,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10427,7 +10427,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10462,11 +10462,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10639,11 +10639,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10672,10 +10672,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 4aebc33710..2a94a4826f 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.138.0 + 7.138.1 Key version Title From 9696acd18c534bcc20a13c5c538990d5d57a2e48 Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Tue, 24 Sep 2024 13:59:52 +0100 Subject: [PATCH 28/46] chery pick returning user fix --- Core/ReturnUserMeasurement.swift | 4 +++ Core/StatisticsLoader.swift | 7 +++--- DuckDuckGoTests/StatisticsLoaderTests.swift | 28 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Core/ReturnUserMeasurement.swift b/Core/ReturnUserMeasurement.swift index ba33d836ab..67b619677b 100644 --- a/Core/ReturnUserMeasurement.swift +++ b/Core/ReturnUserMeasurement.swift @@ -20,8 +20,12 @@ import Foundation import BrowserServicesKit +/// This is only intended to be used during the install (first run after downloading from the app store). protocol ReturnUserMeasurement { + /// Based on the value in the keychain, so if you use this after the install process it will return true. + /// If you really want to know if the user is "returning" then look at the variant in the `StatisticsStore` + /// which will be set to `ru`. var isReturningUser: Bool { get } func installCompletedWithATB(_ atb: Atb) func updateStoredATB(_ atb: Atb) diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index dd8a624685..38255c9097 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -155,9 +155,10 @@ public class StatisticsLoader { private func processUsageSegmentation(atb: Atb?, activityType: UsageActivityType) { guard let installAtbValue = statisticsStore.atb else { return } - let installAtb = Atb(version: installAtbValue, updateVersion: nil) - let actualAtb = atb ?? installAtb - self.usageSegmentation.processATB(actualAtb, withInstallAtb: installAtb, andActivityType: activityType) + let installAtb = Atb(version: installAtbValue + (statisticsStore.variant ?? ""), updateVersion: nil) + let usageAtb = atb ?? installAtb + + self.usageSegmentation.processATB(usageAtb, withInstallAtb: installAtb, andActivityType: activityType) } private func updateUsageSegmentationWithAtb(_ atb: Atb, activityType: UsageActivityType) { diff --git a/DuckDuckGoTests/StatisticsLoaderTests.swift b/DuckDuckGoTests/StatisticsLoaderTests.swift index 063425f707..64dca2b495 100644 --- a/DuckDuckGoTests/StatisticsLoaderTests.swift +++ b/DuckDuckGoTests/StatisticsLoaderTests.swift @@ -42,6 +42,34 @@ class StatisticsLoaderTests: XCTestCase { super.tearDown() } + func testWhenAppRefreshHappensButNotInstalledAndReturningUser_ThenRetentionSegmentationNotified() { + mockStatisticsStore.variant = "ru" + mockStatisticsStore.atb = "v101-1" + + loadSuccessfulExiStub() + + let testExpectation = expectation(description: "refresh complete") + testee.refreshAppRetentionAtb { + testExpectation.fulfill() + } + wait(for: [testExpectation], timeout: 5.0) + XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser) + } + + func testWhenReturnUser_ThenSegmentationIncludesCorrectVariant() { + mockStatisticsStore.variant = "ru" + mockStatisticsStore.atb = "v101-1" + mockStatisticsStore.searchRetentionAtb = "v101-2" + loadSuccessfulAtbStub() + + let testExpectation = expectation(description: "refresh complete") + testee.refreshSearchRetentionAtb { + testExpectation.fulfill() + } + wait(for: [testExpectation], timeout: 5.0) + XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser) + } + func testWhenSearchRefreshHappensButNotInstalled_ThenRetentionSegmentationNotified() { loadSuccessfulExiStub() From fd5804a39f5a22f48fa072f95ab74171def54806 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 24 Sep 2024 14:22:30 +0100 Subject: [PATCH 29/46] Add origin to /apps URL (#3378) Task/Issue URL: https://app.asana.com/0/414235014887631/1208314251851762/f **Description**: Add origin to /apps URL --- Core/AppURLs.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/AppURLs.swift b/Core/AppURLs.swift index e07d771f2a..56542c2066 100644 --- a/Core/AppURLs.swift +++ b/Core/AppURLs.swift @@ -35,7 +35,7 @@ public extension URL { static let emailProtectionSupportLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/email/settings/support"))! static let emailProtectionHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/email-protection/what-is-duckduckgo-email-protection/"))! static let aboutLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/about"))! - static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps"))! + static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps?origin=funnel_app_ios"))! static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))! static let autofillHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/sync-and-backup/password-manager-security/"))! From c472d7598bd6997d76456cd8727822314e1f50f6 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 24 Sep 2024 15:30:15 +0100 Subject: [PATCH 30/46] Release 7.139.0-1 (#3389) --- 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 5cd8288d04..93737696aa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9154,7 +9154,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; @@ -9191,7 +9191,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; @@ -9281,7 +9281,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 = ( @@ -9308,7 +9308,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; @@ -9457,7 +9457,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; @@ -9482,7 +9482,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 = ( @@ -9551,7 +9551,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; @@ -9585,7 +9585,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; @@ -9618,7 +9618,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 = ( @@ -9648,7 +9648,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; @@ -9958,7 +9958,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; @@ -9989,7 +9989,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 = ( @@ -10017,7 +10017,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 = ( @@ -10050,7 +10050,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; @@ -10080,7 +10080,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; @@ -10113,11 +10113,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"; @@ -10350,7 +10350,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; @@ -10377,7 +10377,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; @@ -10409,7 +10409,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; @@ -10446,7 +10446,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; @@ -10481,7 +10481,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; @@ -10516,11 +10516,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"; @@ -10693,11 +10693,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"; @@ -10726,10 +10726,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 d7731d466264e5dde2253e7807c0600518434c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Wed, 25 Sep 2024 12:42:34 +0200 Subject: [PATCH 31/46] Remove Favorites section header from NTP (#3381) Task/Issue URL: https://app.asana.com/0/0/1208244619690577/f Tech Design URL: CC: **Description**: Removes Favorites header when no favorites present. This allowed to removed whole EmptyStateView. Leftover function was incorporated into `FavoritesViewModel`, allowing to remove the generic type from `NewTabPageView`. **Steps to test this PR**: 1. Open NTP without having any favorites, make sure there's an add button and placeholders are visible. 2. Rotate to landscape. Verify whole row is filled up with placeholders. 3. Pixel should be fired when any placeholder is tapped. **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Orientation Testing**: * [x] Portrait * [x] Landscape --- ###### 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 - DuckDuckGo.xcodeproj/project.pbxproj | 44 +--- ...emView.swift => FavoriteAddItemView.swift} | 6 +- DuckDuckGo/FavoriteItem.swift | 5 +- ...wift => FavoritePlaceholderItemView.swift} | 6 +- DuckDuckGo/FavoritesDefaultViewModel.swift | 218 ------------------ DuckDuckGo/FavoritesEmptyStateView.swift | 78 ------- DuckDuckGo/FavoritesPreviewDataSource.swift | 2 +- DuckDuckGo/FavoritesSectionHeader.swift | 49 ---- DuckDuckGo/FavoritesView.swift | 11 +- DuckDuckGo/FavoritesViewModel.swift | 207 +++++++++++++++-- DuckDuckGo/NewTabPageView.swift | 32 +-- DuckDuckGo/NewTabPageViewController.swift | 8 +- DuckDuckGo/UserText.swift | 2 - DuckDuckGo/bg.lproj/Localizable.strings | 3 - DuckDuckGo/cs.lproj/Localizable.strings | 3 - DuckDuckGo/da.lproj/Localizable.strings | 3 - DuckDuckGo/de.lproj/Localizable.strings | 3 - DuckDuckGo/el.lproj/Localizable.strings | 3 - DuckDuckGo/en.lproj/Localizable.strings | 3 - DuckDuckGo/es.lproj/Localizable.strings | 3 - DuckDuckGo/et.lproj/Localizable.strings | 3 - DuckDuckGo/fi.lproj/Localizable.strings | 3 - DuckDuckGo/fr.lproj/Localizable.strings | 3 - DuckDuckGo/hr.lproj/Localizable.strings | 3 - DuckDuckGo/hu.lproj/Localizable.strings | 3 - DuckDuckGo/it.lproj/Localizable.strings | 3 - DuckDuckGo/lt.lproj/Localizable.strings | 3 - DuckDuckGo/lv.lproj/Localizable.strings | 3 - DuckDuckGo/nb.lproj/Localizable.strings | 3 - DuckDuckGo/nl.lproj/Localizable.strings | 3 - DuckDuckGo/pl.lproj/Localizable.strings | 3 - DuckDuckGo/pt.lproj/Localizable.strings | 3 - DuckDuckGo/ro.lproj/Localizable.strings | 3 - DuckDuckGo/ru.lproj/Localizable.strings | 3 - DuckDuckGo/sk.lproj/Localizable.strings | 3 - DuckDuckGo/sl.lproj/Localizable.strings | 3 - DuckDuckGo/sv.lproj/Localizable.strings | 3 - DuckDuckGo/tr.lproj/Localizable.strings | 3 - .../NewTabPageFavoritesModelTests.swift | 55 ++++- 40 files changed, 278 insertions(+), 522 deletions(-) rename DuckDuckGo/{AddFavoritePlaceholderItemView.swift => FavoriteAddItemView.swift} (89%) rename DuckDuckGo/{FavoriteEmptyStateItem.swift => FavoritePlaceholderItemView.swift} (89%) delete mode 100644 DuckDuckGo/FavoritesDefaultViewModel.swift delete mode 100644 DuckDuckGo/FavoritesEmptyStateView.swift delete mode 100644 DuckDuckGo/FavoritesSectionHeader.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index d7ff07905b..5f450c2359 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -766,7 +766,6 @@ extension Pixel { case newTabPageMessageDismissed case newTabPageFavoritesPlaceholderTapped - case newTabPageFavoritesInfoTooltip case newTabPageFavoritesSeeMore case newTabPageFavoritesSeeLess @@ -1581,7 +1580,6 @@ extension Pixel.Event { case .newTabPageMessageDismissed: return "m_new_tab_page_message_dismissed" case .newTabPageFavoritesPlaceholderTapped: return "m_new_tab_page_favorites_placeholder_click" - case .newTabPageFavoritesInfoTooltip: return "m_new_tab_page_favorites_info_tooltip" case .newTabPageFavoritesSeeMore: return "m_new_tab_page_favorites_see_more" case .newTabPageFavoritesSeeLess: return "m_new_tab_page_favorites_see_less" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 93737696aa..a45ae4e43d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -326,10 +326,9 @@ 6FABAA692C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */; }; 6FB1FE9E2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */; }; 6FB1FEA22C256ACD0075B68B /* NewTabPageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */; }; - 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */; }; - 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */; }; + 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */; }; 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */; }; - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */; }; + 6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */; }; 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; }; 6FD0C41F2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */; }; 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */; }; @@ -338,7 +337,6 @@ 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */; }; - 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */; }; 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */; }; 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */; }; 6FD8E51E2C5B84DE00345670 /* NewTabPageIntroMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */; }; @@ -347,8 +345,7 @@ 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */; }; 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */; }; - 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */; }; - 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; + 6FDC64052C98515E00DB71B3 /* FavoriteAddItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */; }; 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127372C20492500EB5724 /* NewTabPage.swift */; }; 6FE1273A2C204BD000EB5724 /* NewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127392C204BD000EB5724 /* NewTabPageView.swift */; }; @@ -1606,10 +1603,9 @@ 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsDebugView.swift; sourceTree = ""; }; 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageManager.swift; sourceTree = ""; }; - 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteEmptyStateItem.swift; sourceTree = ""; }; - 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesEmptyStateView.swift; sourceTree = ""; }; + 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritePlaceholderItemView.swift; sourceTree = ""; }; 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageGridView.swift; sourceTree = ""; }; - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDefaultViewModel.swift; sourceTree = ""; }; + 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetupTests.swift; sourceTree = ""; }; 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewModelTests.swift; sourceTree = ""; }; @@ -1618,7 +1614,6 @@ 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOrientationEnvironmentValue.swift; sourceTree = ""; }; - 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewDataSource.swift; sourceTree = ""; }; 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDelegate.swift; sourceTree = ""; }; 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageView.swift; sourceTree = ""; }; @@ -1627,8 +1622,7 @@ 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroDataStoring.swift; sourceTree = ""; }; 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStore.swift; sourceTree = ""; }; - 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFavoritePlaceholderItemView.swift; sourceTree = ""; }; - 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; + 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteAddItemView.swift; sourceTree = ""; }; 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FE127372C20492500EB5724 /* NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPage.swift; sourceTree = ""; }; 6FE127392C204BD000EB5724 /* NewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageView.swift; sourceTree = ""; }; @@ -3893,10 +3887,9 @@ 6FA3438D2C3D3BB800470677 /* Model */ = { isa = PBXGroup; children = ( - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */, + 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */, 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */, 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, - 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, @@ -3909,7 +3902,8 @@ children = ( 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */, 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */, - 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */, + 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */, + 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */, ); name = Item; sourceTree = ""; @@ -3922,16 +3916,6 @@ name = NewTabPageSectionsDebugView; sourceTree = ""; }; - 6FB2A6782C2C5B9E004D20C8 /* EmptyState */ = { - isa = PBXGroup; - children = ( - 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */, - 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */, - 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */, - ); - name = EmptyState; - sourceTree = ""; - }; 6FD1BAE02B87A0E8000C475C /* AdAttribution */ = { isa = PBXGroup; children = ( @@ -3989,7 +3973,6 @@ 6F691CC82C4979DD002E9553 /* Tooltip */, 6FA343902C3D3C2500470677 /* Item */, 6FA3438D2C3D3BB800470677 /* Model */, - 6FB2A6782C2C5B9E004D20C8 /* EmptyState */, 6FE1273C2C204C2500EB5724 /* FavoritesView.swift */, ); name = Favorites; @@ -7344,7 +7327,6 @@ 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */, 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */, F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, - 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, @@ -7424,7 +7406,7 @@ 6F9FFE302C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, - 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */, + 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */, 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, @@ -7576,7 +7558,6 @@ 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, - 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckPlayerNavigationHandling.swift in Sources */, 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, @@ -7607,7 +7588,7 @@ D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, C1641EB12BC2F52B0012607A /* ImportPasswordsView.swift in Sources */, CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */, - 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */, + 6FDC64052C98515E00DB71B3 /* FavoriteAddItemView.swift in Sources */, 982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */, F446B9B5251150AC00324016 /* HomeMessageViewSectionRenderer.swift in Sources */, D6E0C1852B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift in Sources */, @@ -7697,7 +7678,7 @@ 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */, + 6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -7859,7 +7840,6 @@ D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, - 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */, 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */, F17669D71E43401C003D3222 /* MainViewController.swift in Sources */, 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */, diff --git a/DuckDuckGo/AddFavoritePlaceholderItemView.swift b/DuckDuckGo/FavoriteAddItemView.swift similarity index 89% rename from DuckDuckGo/AddFavoritePlaceholderItemView.swift rename to DuckDuckGo/FavoriteAddItemView.swift index 09b11802c0..85d9b92c1d 100644 --- a/DuckDuckGo/AddFavoritePlaceholderItemView.swift +++ b/DuckDuckGo/FavoriteAddItemView.swift @@ -1,5 +1,5 @@ // -// AddFavoritePlaceholderItemView.swift +// FavoriteAddItemView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -20,7 +20,7 @@ import SwiftUI import DesignResourcesKit -struct AddFavoritePlaceholderItemView: View { +struct FavoriteAddItemView: View { var body: some View { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(.clear) @@ -33,6 +33,6 @@ struct AddFavoritePlaceholderItemView: View { } #Preview { - AddFavoritePlaceholderItemView() + FavoriteAddItemView() .frame(width: 100) } diff --git a/DuckDuckGo/FavoriteItem.swift b/DuckDuckGo/FavoriteItem.swift index 9e6c433d53..0edefc1147 100644 --- a/DuckDuckGo/FavoriteItem.swift +++ b/DuckDuckGo/FavoriteItem.swift @@ -23,6 +23,7 @@ import UniformTypeIdentifiers enum FavoriteItem { case favorite(Favorite) case addFavorite + case placeholder(_ id: String) } extension FavoriteItem: Identifiable { @@ -32,6 +33,8 @@ extension FavoriteItem: Identifiable { return favorite.id case .addFavorite: return "addFavorite" + case .placeholder(let id): + return id } } } @@ -43,7 +46,7 @@ extension FavoriteItem: Reorderable { let itemProvider = NSItemProvider(object: (favorite.urlObject?.absoluteString ?? "") as NSString) let metadata = MoveMetadata(itemProvider: itemProvider, type: .plainText) return .movable(metadata) - case .addFavorite: + case .addFavorite, .placeholder: return .stationary } } diff --git a/DuckDuckGo/FavoriteEmptyStateItem.swift b/DuckDuckGo/FavoritePlaceholderItemView.swift similarity index 89% rename from DuckDuckGo/FavoriteEmptyStateItem.swift rename to DuckDuckGo/FavoritePlaceholderItemView.swift index e5b69afd8f..d009558b07 100644 --- a/DuckDuckGo/FavoriteEmptyStateItem.swift +++ b/DuckDuckGo/FavoritePlaceholderItemView.swift @@ -1,5 +1,5 @@ // -// FavoriteEmptyStateItem.swift +// FavoritePlaceholderItemView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -19,7 +19,7 @@ import SwiftUI -struct FavoriteEmptyStateItem: View { +struct FavoritePlaceholderItemView: View { var body: some View { RoundedRectangle(cornerRadius: 8, style: .continuous) .stroke(Color(designSystemColor: .lines), @@ -29,5 +29,5 @@ struct FavoriteEmptyStateItem: View { } #Preview { - FavoriteEmptyStateItem() + FavoritePlaceholderItemView() } diff --git a/DuckDuckGo/FavoritesDefaultViewModel.swift b/DuckDuckGo/FavoritesDefaultViewModel.swift deleted file mode 100644 index c9088dc5e7..0000000000 --- a/DuckDuckGo/FavoritesDefaultViewModel.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// FavoritesDefaultViewModel.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Bookmarks -import Combine -import SwiftUI -import Core -import WidgetKit - -protocol NewTabPageFavoriteDataSource { - var externalUpdates: AnyPublisher { get } - var favorites: [Favorite] { get } - - func moveFavorite(_ favorite: Favorite, - fromIndex: Int, - toIndex: Int) - - func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? - func favorite(at index: Int) throws -> Favorite? - func removeFavorite(_ favorite: Favorite) -} - -class FavoritesDefaultViewModel: FavoritesViewModel, FavoritesEmptyStateModel { - - @Published private(set) var allFavorites: [FavoriteItem] = [] - @Published private(set) var isCollapsed: Bool = true - @Published private(set) var isShowingTooltip: Bool = false - - private(set) var faviconLoader: FavoritesFaviconLoading? - - private var cancellables = Set() - - private let favoriteDataSource: NewTabPageFavoriteDataSource - private let pixelFiring: PixelFiring.Type - private let dailyPixelFiring: DailyPixelFiring.Type - - var isEmpty: Bool { - allFavorites.filter(\.isFavorite).isEmpty - } - - init(favoriteDataSource: NewTabPageFavoriteDataSource, - faviconLoader: FavoritesFaviconLoading, - pixelFiring: PixelFiring.Type = Pixel.self, - dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { - self.favoriteDataSource = favoriteDataSource - self.pixelFiring = pixelFiring - self.dailyPixelFiring = dailyPixelFiring - self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in - guard let self else { return } - - await MainActor.run { - self.faviconMissing() - } - }) - - - favoriteDataSource.externalUpdates.sink { [weak self] _ in - self?.updateData() - }.store(in: &cancellables) - - updateData() - } - - func toggleCollapse() { - isCollapsed.toggle() - - if isCollapsed { - pixelFiring.fire(.newTabPageFavoritesSeeLess, withAdditionalParameters: [:]) - } else { - pixelFiring.fire(.newTabPageFavoritesSeeMore, withAdditionalParameters: [:]) - } - } - - func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { - let maxCollapsedItemsCount = columnsCount * 2 - let favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites - let isCollapsible = allFavorites.count > maxCollapsedItemsCount - - return .init(items: favorites, isCollapsible: isCollapsible) - } - - // MARK: - External actions - - var onFaviconMissing: () -> Void = {} - func faviconMissing() { - onFaviconMissing() - } - - var onFavoriteURLSelected: ((URL) -> Void)? - func favoriteSelected(_ favorite: Favorite) { - guard let url = favorite.urlObject else { return } - - pixelFiring.fire(.favoriteLaunchedNTP, withAdditionalParameters: [:]) - dailyPixelFiring.fireDaily(.favoriteLaunchedNTPDaily) - Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) - - onFavoriteURLSelected?(url) - } - - var onFavoriteDeleted: ((BookmarkEntity) -> Void)? - func deleteFavorite(_ favorite: Favorite) { - guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } - - pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) - - favoriteDataSource.removeFavorite(favorite) - - WidgetCenter.shared.reloadAllTimelines() - updateData() - - onFavoriteDeleted?(entity) - } - - var onFavoriteEdit: ((BookmarkEntity) -> Void)? - func editFavorite(_ favorite: Favorite) { - guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } - - pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) - - onFavoriteEdit?(entity) - } - - func moveFavorites(from indexSet: IndexSet, to index: Int) { - guard indexSet.count == 1, - let fromIndex = indexSet.first else { return } - - let favoriteItem = allFavorites[fromIndex] - guard case let .favorite(favorite) = favoriteItem else { return } - - favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) - allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) - } - - // MARK: - Empty state model - - func placeholderTapped() { - pixelFiring.fire(.newTabPageFavoritesPlaceholderTapped, withAdditionalParameters: [:]) - } - - func toggleTooltip() { - isShowingTooltip.toggle() - if isShowingTooltip { - pixelFiring.fire(.newTabPageFavoritesInfoTooltip, withAdditionalParameters: [:]) - } - } - - // MARK: - - - private func updateData() { - var allFavorites = favoriteDataSource.favorites.map { - FavoriteItem.favorite($0) - } - allFavorites.append(.addFavorite) - - self.allFavorites = allFavorites - } -} - -enum FavoriteMappingError: Error { - case missingUUID -} - -private final class MissingFaviconWrapper: FavoritesFaviconLoading { - let loader: FavoritesFaviconLoading - - private(set) var onFaviconMissing: (() async -> Void) - - init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { - self.onFaviconMissing = onFaviconMissing - self.loader = loader - } - - func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { - let favicon = await loader.loadFavicon(for: favorite, size: size) - - if favicon == nil { - await onFaviconMissing() - } - - return favicon - } - - func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { - loader.fakeFavicon(for: favorite, size: size) - } - - func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { - loader.existingFavicon(for: favorite, size: size) - } -} - -private extension FavoriteItem { - var isFavorite: Bool { - switch self { - case .favorite: - return true - case .addFavorite: - return false - } - } -} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift deleted file mode 100644 index 8c81ce6765..0000000000 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// FavoritesEmptyStateView.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 DuckUI - -struct FavoritesEmptyStateView: View { - @ObservedObject var model: Model - @Binding var isAddingFavorite: Bool - - let geometry: GeometryProxy? - - var body: some View { - ZStack(alignment: .topTrailing) { - VStack(spacing: 16) { - FavoritesSectionHeader(model: model) - - NewTabPageGridView(geometry: geometry) { placeholdersCount in - Button(action: { - isAddingFavorite = true - }, label: { - AddFavoritePlaceholderItemView() - }) - .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) - .frame(width: NewTabPageGrid.Item.edgeSize) - - let placeholders = Array(0..: View { .frame(width: NewTabPageGrid.Item.edgeSize) .previewShape() .transition(.opacity) - case .addFavorite: + case .addFavorite, .placeholder: EmptyView() } } @@ -110,10 +110,17 @@ struct FavoritesView: View { Button(action: { isAddingFavorite = true }, label: { - AddFavoritePlaceholderItemView() + FavoriteAddItemView() }) .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) .frame(width: NewTabPageGrid.Item.edgeSize) + case .placeholder: + FavoritePlaceholderItemView() + .frame(width: NewTabPageGrid.Item.edgeSize, height: NewTabPageGrid.Item.edgeSize) + .contentShape(.rect) + .onTapGesture { + model.placeholderTapped() + } } } } diff --git a/DuckDuckGo/FavoritesViewModel.swift b/DuckDuckGo/FavoritesViewModel.swift index 624186edb6..781d77a044 100644 --- a/DuckDuckGo/FavoritesViewModel.swift +++ b/DuckDuckGo/FavoritesViewModel.swift @@ -18,37 +18,204 @@ // import Foundation +import Bookmarks +import Combine +import SwiftUI +import Core +import WidgetKit -protocol FavoritesViewModel: AnyObject, ObservableObject { - var allFavorites: [FavoriteItem] { get } - var faviconLoader: FavoritesFaviconLoading? { get } +protocol NewTabPageFavoriteDataSource { + var externalUpdates: AnyPublisher { get } + var favorites: [Favorite] { get } - var isEmpty: Bool { get } - var isCollapsed: Bool { get } + func moveFavorite(_ favorite: Favorite, + fromIndex: Int, + toIndex: Int) - func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice + func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? + func favorite(at index: Int) throws -> Favorite? + func removeFavorite(_ favorite: Favorite) +} + +struct FavoritesSlice { + let items: [FavoriteItem] + let isCollapsible: Bool +} + +class FavoritesViewModel: ObservableObject { + + @Published private(set) var allFavorites: [FavoriteItem] = [] + @Published private(set) var isCollapsed: Bool = true + + private(set) var faviconLoader: FavoritesFaviconLoading? + + private var cancellables = Set() + + private let favoriteDataSource: NewTabPageFavoriteDataSource + private let pixelFiring: PixelFiring.Type + private let dailyPixelFiring: DailyPixelFiring.Type + + var isEmpty: Bool { + allFavorites.filter(\.isFavorite).isEmpty + } + + init(favoriteDataSource: NewTabPageFavoriteDataSource, + faviconLoader: FavoritesFaviconLoading, + pixelFiring: PixelFiring.Type = Pixel.self, + dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { + self.favoriteDataSource = favoriteDataSource + self.pixelFiring = pixelFiring + self.dailyPixelFiring = dailyPixelFiring + self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in + guard let self else { return } + + await MainActor.run { + self.faviconMissing() + } + }) + + + favoriteDataSource.externalUpdates.sink { [weak self] _ in + self?.updateData() + }.store(in: &cancellables) + + updateData() + } + + func toggleCollapse() { + isCollapsed.toggle() + + if isCollapsed { + pixelFiring.fire(.newTabPageFavoritesSeeLess, withAdditionalParameters: [:]) + } else { + pixelFiring.fire(.newTabPageFavoritesSeeMore, withAdditionalParameters: [:]) + } + } + + func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + let hasFavorites = allFavorites.contains(where: \.isFavorite) + let maxCollapsedItemsCount = hasFavorites ? columnsCount * 2 : columnsCount + let isCollapsible = allFavorites.count > maxCollapsedItemsCount + + var favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites + + if !hasFavorites { + for _ in favorites.count ..< maxCollapsedItemsCount { + favorites.append(.placeholder(UUID().uuidString)) + } + } + + return .init(items: favorites, isCollapsible: isCollapsible) + } + + // MARK: - External actions + + var onFaviconMissing: () -> Void = {} + func faviconMissing() { + onFaviconMissing() + } - func faviconMissing() + var onFavoriteURLSelected: ((URL) -> Void)? + func favoriteSelected(_ favorite: Favorite) { + guard let url = favorite.urlObject else { return } - // MARK: - Interactions + pixelFiring.fire(.favoriteLaunchedNTP, withAdditionalParameters: [:]) + dailyPixelFiring.fireDaily(.favoriteLaunchedNTPDaily) + Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) - func toggleCollapse() + onFavoriteURLSelected?(url) + } - func favoriteSelected(_ favorite: Favorite) - func editFavorite(_ favorite: Favorite) - func deleteFavorite(_ favorite: Favorite) - func moveFavorites(from indexSet: IndexSet, to index: Int) + var onFavoriteDeleted: ((BookmarkEntity) -> Void)? + func deleteFavorite(_ favorite: Favorite) { + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } + + pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) + + favoriteDataSource.removeFavorite(favorite) + + WidgetCenter.shared.reloadAllTimelines() + updateData() + + onFavoriteDeleted?(entity) + } + + var onFavoriteEdit: ((BookmarkEntity) -> Void)? + func editFavorite(_ favorite: Favorite) { + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } + + pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) + + onFavoriteEdit?(entity) + } + + func moveFavorites(from indexSet: IndexSet, to index: Int) { + guard indexSet.count == 1, + let fromIndex = indexSet.first else { return } + + let favoriteItem = allFavorites[fromIndex] + guard case let .favorite(favorite) = favoriteItem else { return } + + favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) + allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) + } + + func placeholderTapped() { + pixelFiring.fire(.newTabPageFavoritesPlaceholderTapped, withAdditionalParameters: [:]) + } + + // MARK: - + + private func updateData() { + var allFavorites = favoriteDataSource.favorites.map { + FavoriteItem.favorite($0) + } + allFavorites.append(.addFavorite) + + self.allFavorites = allFavorites + } +} + +enum FavoriteMappingError: Error { + case missingUUID } -protocol FavoritesEmptyStateModel: AnyObject, ObservableObject { +private final class MissingFaviconWrapper: FavoritesFaviconLoading { + let loader: FavoritesFaviconLoading + + private(set) var onFaviconMissing: (() async -> Void) + + init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { + self.onFaviconMissing = onFaviconMissing + self.loader = loader + } - var isShowingTooltip: Bool { get } + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + let favicon = await loader.loadFavicon(for: favorite, size: size) - func placeholderTapped() - func toggleTooltip() + if favicon == nil { + await onFaviconMissing() + } + + return favicon + } + + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + loader.fakeFavicon(for: favorite, size: size) + } + + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + loader.existingFavicon(for: favorite, size: size) + } } -struct FavoritesSlice { - let items: [FavoriteItem] - let isCollapsible: Bool +private extension FavoriteItem { + var isFavorite: Bool { + switch self { + case .favorite: + return true + case .addFavorite, .placeholder: + return false + } + } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index 14d97ce3e2..aaed282ca5 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -21,12 +21,12 @@ import SwiftUI import DuckUI import RemoteMessaging -struct NewTabPageView: View { +struct NewTabPageView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject private var viewModel: NewTabPageViewModel @ObservedObject private var messagesModel: NewTabPageMessagesModel - @ObservedObject private var favoritesModel: FavoritesModelType + @ObservedObject private var favoritesViewModel: FavoritesViewModel @ObservedObject private var shortcutsModel: ShortcutsModel @ObservedObject private var shortcutsSettingsModel: NewTabPageShortcutsSettingsModel @ObservedObject private var sectionsSettingsModel: NewTabPageSectionsSettingsModel @@ -36,13 +36,13 @@ struct NewTabPageView some View { - Group { - if favoritesModel.isEmpty { - FavoritesEmptyStateView(model: favoritesModel, - isAddingFavorite: $isAddingFavorite, - geometry: proxy) - .padding(.top, Metrics.nonGridSectionTopPadding) - } else { - FavoritesView(model: favoritesModel, + FavoritesView(model: favoritesViewModel, isAddingFavorite: $isAddingFavorite, geometry: proxy) - } - } } @ViewBuilder @@ -274,7 +260,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -299,7 +285,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { ] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -314,7 +300,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(favorites: []), + favoritesViewModel: FavoritesPreviewModel(favorites: []), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -329,7 +315,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel(storage: .emptyStorage()) diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index fabd1ee3ed..22a238bc23 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -23,7 +23,7 @@ import Bookmarks import BrowserServicesKit import Core -final class NewTabPageViewController: UIHostingController>, NewTabPage { +final class NewTabPageViewController: UIHostingController, NewTabPage { private let syncService: DDGSyncing private let syncBookmarksAdapter: SyncBookmarksAdapter @@ -35,7 +35,7 @@ final class NewTabPageViewController: UIHostingController FavoritesDefaultViewModel { - FavoritesDefaultViewModel(favoriteDataSource: favoriteDataSource, - faviconLoader: FavoritesFaviconLoader(), - pixelFiring: PixelFiringMock.self, - dailyPixelFiring: PixelFiringMock.self) + func testPrefixFavoritesLimitsToTwoRows() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT() + + let slice = sut.prefixedFavorites(for: 4) + + XCTAssertEqual(slice.items.count, 8) + XCTAssertTrue(slice.isCollapsible) + } + + func testAddItemIsLastWhenFavoritesPresent() throws { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT() + + let lastItem = try XCTUnwrap(sut.allFavorites.last) + + XCTAssertTrue(lastItem == .addFavorite) + } + + func testAddItemIsFirstWhenFavoritesEmpty() throws { + let sut = createSUT() + + let firstItem = try XCTUnwrap(sut.allFavorites.first) + + XCTAssertTrue(firstItem == .addFavorite) + } + + private func createSUT() -> FavoritesViewModel { + FavoritesViewModel(favoriteDataSource: favoriteDataSource, + faviconLoader: FavoritesFaviconLoader(), + pixelFiring: PixelFiringMock.self, + dailyPixelFiring: PixelFiringMock.self) } } @@ -129,3 +157,12 @@ private extension Favorite { Favorite(id: UUID().uuidString, title: "foo", domain: "bar") } } + +private extension FavoriteItem { + var isPlaceholder: Bool { + switch self { + case .placeholder: return true + case .favorite, .addFavorite: return false + } + } +} From b2fd9717e9a5a2264a54bcabf421244f096ce160 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Wed, 25 Sep 2024 14:33:19 +0100 Subject: [PATCH 32/46] translations for Switch to Tab (#3391) Task/Issue URL: https://app.asana.com/0/1206767823185684/1208303424401439/f Tech Design URL: CC: **Description**: Add translations for Switch to Tab. **Steps to test this PR**: 1. Make sure build is green --- DuckDuckGo/bg.lproj/Localizable.strings | 3 +++ DuckDuckGo/cs.lproj/Localizable.strings | 3 +++ DuckDuckGo/da.lproj/Localizable.strings | 3 +++ DuckDuckGo/de.lproj/Localizable.strings | 3 +++ DuckDuckGo/el.lproj/Localizable.strings | 3 +++ DuckDuckGo/es.lproj/Localizable.strings | 3 +++ DuckDuckGo/et.lproj/Localizable.strings | 3 +++ DuckDuckGo/fi.lproj/Localizable.strings | 3 +++ DuckDuckGo/fr.lproj/Localizable.strings | 3 +++ DuckDuckGo/hr.lproj/Localizable.strings | 3 +++ DuckDuckGo/hu.lproj/Localizable.strings | 3 +++ DuckDuckGo/it.lproj/Localizable.strings | 3 +++ DuckDuckGo/lt.lproj/Localizable.strings | 3 +++ DuckDuckGo/lv.lproj/Localizable.strings | 3 +++ DuckDuckGo/nb.lproj/Localizable.strings | 3 +++ DuckDuckGo/nl.lproj/Localizable.strings | 3 +++ DuckDuckGo/pl.lproj/Localizable.strings | 3 +++ DuckDuckGo/pt.lproj/Localizable.strings | 3 +++ DuckDuckGo/ro.lproj/Localizable.strings | 3 +++ DuckDuckGo/ru.lproj/Localizable.strings | 3 +++ DuckDuckGo/sk.lproj/Localizable.strings | 3 +++ DuckDuckGo/sl.lproj/Localizable.strings | 3 +++ DuckDuckGo/sv.lproj/Localizable.strings | 3 +++ DuckDuckGo/tr.lproj/Localizable.strings | 3 +++ 24 files changed, 72 insertions(+) diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index 08cabf8dbb..28f226f39d 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Същата поверителност.\nПо-добри предложения при търсене!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Превключване към раздел"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Деактивирано"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index 2ee7508f47..089d695f78 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Stejné soukromí.\nLepší návrhy pro vyhledávání!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Přepnout na kartu"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Zakázáno"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index e15bd57e10..b4a5cdd89d 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Samme fortrolighed.\nBedre søgeforslag!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Skift til fane"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Deaktiveret"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 9a403f38c1..0b04f1dfcb 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Gleiche Privatsphäre.\nBessere Suchvorschläge!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Zu Tab wechseln"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Deaktiviert"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 593f65c6b6..6606ee33a3 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Ίδιο απόρρητο.\nΚαλύτερες προτάσεις αναζήτησης!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Αλλαγή στην Καρτέλα"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Απενεργοποιήθηκε"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 397ff70979..1b75d2cac7 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Misma privacidad.\n¡Mejores sugerencias de búsqueda!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Cambiar a Pestaña"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Desactivado"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index e016626981..1e72c63aec 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Sama privaatsus.\nParemad otsingusoovitused!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Vaheta vahekaardile"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Keelatud"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index f56d2050e4..f3689a274a 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Sama tietoja,\nparempia hakuehdotuksia!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Vaihda välilehteen"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Pois käytöstä"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index c0935e7ba1..5eaba8d70c 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "La confidentialité reste la même.\nLes suggestions de recherche s'améliorent !"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Passer à l'onglet"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Désactivé"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 563b34bdc0..e762b4d00f 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Ista privatnost.\nBolji prijedlozi za pretraživanje!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Prebaci se na karticu"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Onemogućeno"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 3399b62a5b..c6a6b91164 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Ugyanolyan adatvédelem.\nJobb keresési javaslatok!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Váltás lapra"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Letiltva"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index 4655aa644b..4f14d0d1d1 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "La stessa privacy.\nSuggerimenti di ricerca migliori!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Passa alla scheda"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Disattivato"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index bffebb0cd7..cc7383f995 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Tas pats privatumas.\nGeresni paieškos pasiūlymai!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Perjungti skirtuką"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Išjungta"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 91f43d110d..6aa8661114 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Tas pats privātums.\nLabāki meklēšanas ieteikumi!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Pārslēgties uz cilni"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Atspējota"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index 907ef03667..21b148b717 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Samme personvern.\nBedre søkeforslag!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Bytt til fane"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Deaktivert"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index 4078831a30..3bad0bbe3b 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Dezelfde privacy.\nBetere zoeksuggesties!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Overschakelen naar tabblad"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Uitgeschakeld"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 806d45b3b2..24176d0cfd 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Ta sama prywatność.\nLepsze sugestie podczas wyszukiwania!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Przełącz na kartę"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Wyłączone"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index d212dbd9a3..808a4577a5 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "A mesma privacidade.\nMelhores sugestões de pesquisa!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Mudar para separador"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Desativado"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index 6a4093c3aa..e544c45d0a 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Aceeași confidențialitate.\nSugestii de căutare mai bune!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Comutați la filă"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Dezactivat"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 57442b594e..082fc1c61a 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Та же конфиденциальность.\nЛучшие поисковые рекомендации!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Перейти на вкладку"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Отключено"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 5811b9f9d7..7d3fc58141 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Rovnaké súkromie.\nLepšie návrhy vyhľadávania!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Prepnúť na kartu"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Zakázané"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 49331cce74..39f89204b9 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Enaka raven zasebnosti.\nBoljši predlogi iskanja!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Preklopite na zavihek"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Onemogočeno"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index 39074f10d3..5385fcc294 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Samma integritet.\nBättre sökförslag!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Byt till flik"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Inaktiverad"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index 348ada4d1f..e9453b557e 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -256,6 +256,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Aynı gizlilik.\nDaha iyi arama önerileri!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Sekmeye Geç"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Devre Dışı"; From ad720edb56ad4a6a8e6e60f702bf98b352b60292 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 25 Sep 2024 17:49:41 +0200 Subject: [PATCH 33/46] Bump C.S.S --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a45ae4e43d..fb96a2dc14 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10918,8 +10918,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 198.0.1; + branch = daniel/bump.css; + kind = branch; }; }; 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 4febefbd6a..90d5c69c48 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" : "cc3629fa16880e410e588a27a6b2426dcc140009", - "version" : "198.0.1" + "branch" : "daniel/bump.css", + "revision" : "09289dbd827cd14559713cd4a2f314d6cfb753c5" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "4f0d109f13beec7e8beaf0bd5c0e2c1d528677f8", - "version" : "6.16.0" + "revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1", + "version" : "6.17.0" } }, { From 0669c36b287f2841aa7c41993e5057fcf1e36fcc Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 25 Sep 2024 17:52:22 +0200 Subject: [PATCH 34/46] Revert "Bump C.S.S" This reverts commit ad720edb56ad4a6a8e6e60f702bf98b352b60292. --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index fb96a2dc14..a45ae4e43d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10918,8 +10918,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - branch = daniel/bump.css; - kind = branch; + kind = exactVersion; + version = 198.0.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { From 31eca1e98de53ff0817addfb09367b943291b380 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 25 Sep 2024 21:39:54 +0200 Subject: [PATCH 35/46] Rever BSK branch --- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 90d5c69c48..4febefbd6a 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" : { - "branch" : "daniel/bump.css", - "revision" : "09289dbd827cd14559713cd4a2f314d6cfb753c5" + "revision" : "cc3629fa16880e410e588a27a6b2426dcc140009", + "version" : "198.0.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1", - "version" : "6.17.0" + "revision" : "4f0d109f13beec7e8beaf0bd5c0e2c1d528677f8", + "version" : "6.16.0" } }, { From b7e0c626ce4be29bf52820bd69b5bf9e301f8e3f Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 25 Sep 2024 23:25:59 +0200 Subject: [PATCH 36/46] Bump BSK which includes C.S.S 6.17 (#3395) Task/Issue URL: https://app.asana.com/0/414235014887631/1208399924957022/f **Description**: - Bumps C.S.S. --- 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 a45ae4e43d..67e79b0a99 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10919,7 +10919,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.0.1; + version = 198.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4febefbd6a..d8b3ac6978 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" : "cc3629fa16880e410e588a27a6b2426dcc140009", - "version" : "198.0.1" + "revision" : "4db50292abf1180d66da55cf83f75d37395df1f9", + "version" : "198.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "4f0d109f13beec7e8beaf0bd5c0e2c1d528677f8", - "version" : "6.16.0" + "revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1", + "version" : "6.17.0" } }, { From f723a5cda8fa4cbe5887c8294c262612cc50b8ca Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 25 Sep 2024 23:26:35 +0200 Subject: [PATCH 37/46] Bump BSK to Include C.S.S 6.17 (#3397) Task/Issue URL: https://app.asana.com/0/414235014887631/1208399924957022/f **Description**: - Updates BSK to 198.1.0 which includes C.S.S 6.17.0, fixing a DuckPlayer issue --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 93737696aa..a4276b25f9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10939,7 +10939,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.0.1; + version = 198.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { From b95013e589916458e7f2d19f395d44c143a3f4e8 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 26 Sep 2024 00:06:22 +0200 Subject: [PATCH 38/46] Release 7.139.0-2 (#3398) --- 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 a4276b25f9..10c3a6b2c2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9154,7 +9154,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9191,7 +9191,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9281,7 +9281,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9308,7 +9308,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9457,7 +9457,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9482,7 +9482,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9551,7 +9551,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9585,7 +9585,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9618,7 +9618,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9648,7 +9648,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9958,7 +9958,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9989,7 +9989,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10017,7 +10017,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10050,7 +10050,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10080,7 +10080,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10113,11 +10113,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10350,7 +10350,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10377,7 +10377,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10409,7 +10409,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10446,7 +10446,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10481,7 +10481,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10516,11 +10516,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10693,11 +10693,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10726,10 +10726,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 63a18b721a332072afd7b32c264300f84c0bab6c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 26 Sep 2024 00:06:04 +0100 Subject: [PATCH 39/46] add assertions for tabs in suggestions (#3394) Task/Issue URL: https://app.asana.com/0/392891325557410/1208350972189287/f Tech Design URL: CC: @ayoy **Description**: Adds assertions that validate tabs in suggestions and switching tabs Fixes failure check for sending notification in the workflow **Steps to test this PR**: 1. Check this test run passes: https://github.com/duckduckgo/iOS/actions/runs/11037282165 (However End to End tests have not passed for some time so as long as tabs.yaml passes that is the main thing) 3. Run the release/tabs.yaml test locally --- .github/workflows/end-to-end.yml | 1 - .maestro/release_tests/tabs.yaml | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index 3576b629c8..3e6b9b658b 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -103,7 +103,6 @@ jobs: steps: - name: Create Asana task when workflow failed - if: ${{ failure() }} run: | curl -s "https://app.asana.com/api/1.0/tasks" \ --header "Accept: application/json" \ diff --git a/.maestro/release_tests/tabs.yaml b/.maestro/release_tests/tabs.yaml index 40ae909d64..72618da2e2 100644 --- a/.maestro/release_tests/tabs.yaml +++ b/.maestro/release_tests/tabs.yaml @@ -46,6 +46,27 @@ tags: - assertVisible: ".*Privacy Test Pages.*" - tapOn: "Refresh Page" +# Suggestions +- assertVisible: + id: "searchEntry" + +- tapOn: + id: "searchEntry" +- inputText: "ad click" +- assertVisible: "Switch to Tab.*search-company.site" +- tapOn: "Switch to Tab.*search-company.site" +- assertVisible: ".*Ad Click Flow.*" + +- tapOn: + id: "searchEntry" +- inputText: "privacy" +- assertVisible: "Switch to Tab.*privacy-test-pages.site" +- tapOn: "Switch to Tab.*privacy-test-pages.site" +- assertVisible: ".*Privacy Test Pages.*" + +# Needed or else test can't see the Tab Switcher button for some reason +- tapOn: "Refresh Page" + # Close Tab - assertVisible: Tab Switcher - tapOn: Tab Switcher @@ -57,3 +78,26 @@ tags: - assertNotVisible: ".*Ad Click Flow.*" - assertVisible: "1 Private Tab" - tapOn: "Done" + +# Switch tabs from new tab +- tapOn: "Refresh Page" +- assertVisible: Tab Switcher +- tapOn: Tab Switcher +- assertVisible: ".*Privacy Test Pages.*" +- assertVisible: + id: "Add" +- tapOn: + id: "Add" +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "privacy" +- assertVisible: "Switch to Tab.*privacy-test-pages.site" +- tapOn: "Switch to Tab.*privacy-test-pages.site" +- assertVisible: ".*Privacy Test Pages.*" +- tapOn: "Refresh Page" +- assertVisible: Tab Switcher +- tapOn: Tab Switcher +- assertVisible: "1 Private Tab" + From 3184192f8d240a570902beba085cd2faffec369b Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 26 Sep 2024 20:52:20 +1000 Subject: [PATCH 40/46] Onboarding Highlights Ship Review (#3380) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208291253556033/f **Description**: This PR includes changes and fixes that have been addressed during the Ship Review. --- .maestro/shared/onboarding.yaml | 10 + Core/Debouncer.swift | 66 ++++++ Core/DefaultVariantManager.swift | 7 +- Core/NSAttributedStringExtension.swift | 28 ++- DuckDuckGo.xcodeproj/project.pbxproj | 16 ++ .../xcshareddata/swiftpm/Package.resolved | 8 +- DuckDuckGo/DaxDialogs.swift | 35 +++- DuckDuckGo/HomeViewController.swift | 8 +- DuckDuckGo/MainViewController+Segues.swift | 2 +- DuckDuckGo/MainViewController.swift | 13 +- DuckDuckGo/NewTabPageViewController.swift | 6 +- .../OnboardingAddressBarPositionPicker.swift | 39 ++-- .../AppIconPicker/AppIconPicker.swift | 2 +- .../ContextualOnboardingDialogs.swift | 6 +- .../ContextualDaxDialogsFactory.swift | 15 +- .../ContextualOnboardingPresenter.swift | 7 +- .../DefaultVariantManager+Onboarding.swift | 43 ++++ .../Manager/OnboardingManager.swift | 7 +- .../OnboardingView+AppIconPickerContent.swift | 1 - .../ProgressBarView.swift | 4 +- .../OnboardingSuggestedSearchesProvider.swift | 16 +- DuckDuckGo/TabDelegate.swift | 2 - DuckDuckGo/TabSwitcherViewController.swift | 31 ++- DuckDuckGo/TabViewController.swift | 21 +- DuckDuckGo/TabsBarViewController.swift | 11 +- DuckDuckGo/UserText.swift | 2 +- DuckDuckGo/bg.lproj/Localizable.strings | 66 +++++- DuckDuckGo/cs.lproj/Localizable.strings | 68 +++++- DuckDuckGo/da.lproj/Localizable.strings | 68 +++++- DuckDuckGo/de.lproj/Localizable.strings | 68 +++++- DuckDuckGo/el.lproj/Localizable.strings | 68 +++++- DuckDuckGo/en.lproj/Localizable.strings | 2 +- DuckDuckGo/es.lproj/Localizable.strings | 68 +++++- DuckDuckGo/et.lproj/Localizable.strings | 68 +++++- DuckDuckGo/fi.lproj/Localizable.strings | 68 +++++- DuckDuckGo/fr.lproj/Localizable.strings | 68 +++++- DuckDuckGo/hr.lproj/Localizable.strings | 68 +++++- DuckDuckGo/hu.lproj/Localizable.strings | 68 +++++- DuckDuckGo/it.lproj/Localizable.strings | 68 +++++- DuckDuckGo/lt.lproj/Localizable.strings | 68 +++++- DuckDuckGo/lv.lproj/Localizable.strings | 68 +++++- DuckDuckGo/nb.lproj/Localizable.strings | 68 +++++- DuckDuckGo/nl.lproj/Localizable.strings | 68 +++++- DuckDuckGo/pl.lproj/Localizable.strings | 68 +++++- DuckDuckGo/pt.lproj/Localizable.strings | 68 +++++- DuckDuckGo/ro.lproj/Localizable.strings | 68 +++++- DuckDuckGo/ru.lproj/Localizable.strings | 68 +++++- DuckDuckGo/sk.lproj/Localizable.strings | 68 +++++- DuckDuckGo/sl.lproj/Localizable.strings | 68 +++++- DuckDuckGo/sv.lproj/Localizable.strings | 68 +++++- DuckDuckGo/tr.lproj/Localizable.strings | 68 +++++- .../ContextualOnboardingPresenterTests.swift | 43 +++- DuckDuckGoTests/DaxDialogTests.swift | 82 +++++++- DuckDuckGoTests/DaxDialogsNewTabTests.swift | 12 ++ DuckDuckGoTests/DebouncerTests.swift | 81 +++++++ ...DefaultVariantManagerOnboardingTests.swift | 198 ++++++++++++++++++ .../HomeViewControllerDaxDialogTests.swift | 74 ++++++- DuckDuckGoTests/MockTabDelegate.swift | 2 - DuckDuckGoTests/OnboardingManagerTests.swift | 33 ++- ...ardingSuggestedSearchesProviderTests.swift | 19 +- .../TabViewControllerDaxDialogTests.swift | 23 +- 61 files changed, 2386 insertions(+), 219 deletions(-) create mode 100644 Core/Debouncer.swift create mode 100644 DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift create mode 100644 DuckDuckGoTests/DebouncerTests.swift create mode 100644 DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift diff --git a/.maestro/shared/onboarding.yaml b/.maestro/shared/onboarding.yaml index 17bf8ffb35..2117d4205b 100644 --- a/.maestro/shared/onboarding.yaml +++ b/.maestro/shared/onboarding.yaml @@ -14,3 +14,13 @@ appId: com.duckduckgo.mobile.ios # - assertVisible: "Make DuckDuckGo your default browser." - tapOn: text: "Skip" + +- runFlow: + when: + visible: "Which color looks best on me?" + commands: + - assertVisible: "Next" + - tapOn: "Next" + - assertVisible: "Where should I put your address bar?" + - assertVisible: "Next" + - tapOn: "Next" diff --git a/Core/Debouncer.swift b/Core/Debouncer.swift new file mode 100644 index 0000000000..094737b956 --- /dev/null +++ b/Core/Debouncer.swift @@ -0,0 +1,66 @@ +// +// Debouncer.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A class that provides a debouncing mechanism. +public final class Debouncer { + private let runLoop: RunLoop + private let mode: RunLoop.Mode + private var timer: Timer? + + /// Initializes a new instance of `Debouncer`. + /// + /// - Parameters: + /// - runLoop: The `RunLoop` on which the debounced actions will be scheduled. Defaults to the current run loop. + /// + /// - mode: The `RunLoop.Mode` in which the debounced actions will be scheduled. Defaults to `.default`. + /// + /// Use `RunLoop.main` for UI-related actions to ensure they run on the main thread. + public init(runLoop: RunLoop = .current, mode: RunLoop.Mode = .default) { + self.runLoop = runLoop + self.mode = mode + } + + /// Debounces the provided block of code, executing it after a specified time interval elapses. + /// - Parameters: + /// - dueTime: The time interval (in seconds) to wait before executing the block. + /// - block: The closure to execute after the due time has passed. + /// + /// If `dueTime` is less than or equal to zero, the block is executed immediately. + public func debounce(for dueTime: TimeInterval, block: @escaping () -> Void) { + timer?.invalidate() + + guard dueTime > 0 else { return block() } + + let timer = Timer(timeInterval: dueTime, repeats: false, block: { timer in + guard timer.isValid else { return } + block() + }) + + runLoop.add(timer, forMode: mode) + self.timer = timer + } + + /// Cancels any pending execution of the debounced block. + public func cancel() { + timer?.invalidate() + timer = nil + } +} diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index 5ce128d6ec..76f0f8475a 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -28,6 +28,8 @@ extension FeatureName { // public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature") public static let newOnboardingIntro = FeatureName(rawValue: "newOnboardingIntro") + public static let newOnboardingIntroHighlights = FeatureName(rawValue: "newOnboardingIntroHighlights") + public static let contextualDaxDialogs = FeatureName(rawValue: "contextualDaxDialogs") } public struct VariantIOS: Variant { @@ -56,8 +58,9 @@ public struct VariantIOS: Variant { VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []), - VariantIOS(name: "ma", weight: 1, isIncluded: When.always, features: []), - VariantIOS(name: "mb", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]), + VariantIOS(name: "ms", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]), + VariantIOS(name: "mu", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro, .contextualDaxDialogs]), + VariantIOS(name: "mx", weight: 1, isIncluded: When.always, features: [.newOnboardingIntroHighlights, .contextualDaxDialogs]), returningUser ] diff --git a/Core/NSAttributedStringExtension.swift b/Core/NSAttributedStringExtension.swift index c4beb2b327..1660f27721 100644 --- a/Core/NSAttributedStringExtension.swift +++ b/Core/NSAttributedStringExtension.swift @@ -84,6 +84,26 @@ extension NSAttributedString { } } +// MARK: - AttributedString Helper Extensions + +public extension String { + + var attributed: NSAttributedString { + NSAttributedString(string: self) + } + + var nsRange: NSRange { + NSRange(startIndex..., in: self) + } + + func range(of string: String) -> NSRange { + (self as NSString).range(of: string) + } + +} + +// MARK: Helper Operators + /// Concatenates two `NSAttributedString` instances, returning a new `NSAttributedString`. /// /// - Parameters: @@ -115,11 +135,3 @@ public func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString { public func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString { NSAttributedString(string: lhs) + rhs } - -private extension String { - - var nsRange: NSRange { - NSRange(startIndex..., in: self) - } - -} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 10c3a6b2c2..f9a6a04a9e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -695,6 +695,9 @@ 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; + 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; + 9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */; }; + 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; @@ -709,6 +712,7 @@ 9F5E5AAC2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AAB2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift */; }; 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AAF2C3E4C6000165F54 /* ContextualOnboardingPresenter.swift */; }; 9F5E5AB22C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AB12C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift */; }; + 9F678B892C9BAA4800CA0E19 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F678B882C9BAA4800CA0E19 /* Debouncer.swift */; }; 9F6933192C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6933182C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift */; }; 9F69331B2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */; }; 9F69331D2C5A191400CD6A5D /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */; }; @@ -2503,6 +2507,9 @@ 98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = ""; }; 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = ""; }; + 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; + 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultVariantManager+Onboarding.swift"; sourceTree = ""; }; + 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVariantManagerOnboardingTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; @@ -2517,6 +2524,7 @@ 9F5E5AAB2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualDaxDialogsFactory.swift; sourceTree = ""; }; 9F5E5AAF2C3E4C6000165F54 /* ContextualOnboardingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenter.swift; sourceTree = ""; }; 9F5E5AB12C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterTests.swift; sourceTree = ""; }; + 9F678B882C9BAA4800CA0E19 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; 9F6933182C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterMock.swift; sourceTree = ""; }; 9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDaxFavouritesTests.swift; sourceTree = ""; }; 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTutorialSettings.swift; sourceTree = ""; }; @@ -4773,6 +4781,7 @@ 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */, 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */, 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */, + 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */, ); name = Onboarding; sourceTree = ""; @@ -4898,6 +4907,7 @@ 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, 9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */, 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */, + 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */, ); path = OnboardingExperiment; sourceTree = ""; @@ -5901,6 +5911,7 @@ 56D855692BEA9169009F9698 /* CurrentDateProviding.swift */, 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */, 9FEA22312C3270BD006B03BF /* TimerInterface.swift */, + 9F678B882C9BAA4800CA0E19 /* Debouncer.swift */, ); name = Utilities; sourceTree = ""; @@ -6047,6 +6058,7 @@ 8341D804212D5DFB000514C2 /* HashExtensionTest.swift */, 1CB7B82223CEA28300AA24EA /* DateExtensionTests.swift */, 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */, + 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */, ); name = Utilities; sourceTree = ""; @@ -7428,6 +7440,7 @@ 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, + 9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, @@ -7892,6 +7905,7 @@ files = ( 8528AE84212FF9A100D0BD74 /* AppRatingPromptStorageTests.swift in Sources */, 569437312BE3F64400C0881B /* SyncErrorHandlerSyncPausedAlertsTests.swift in Sources */, + 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */, 1CB7B82323CEA28300AA24EA /* DateExtensionTests.swift in Sources */, 31C138A427A3352600FFD4B2 /* DownloadTests.swift in Sources */, 853A717820F645FB00FE60BC /* PixelTests.swift in Sources */, @@ -7947,6 +7961,7 @@ 5694372B2BE3F2D900C0881B /* SyncErrorHandlerTests.swift in Sources */, 987130C7294AAB9F00AB05E0 /* MenuBookmarksViewModelTests.swift in Sources */, 858650D32469BFAD00C36F8A /* DaxDialogTests.swift in Sources */, + 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */, 31C138B227A4097800FFD4B2 /* DownloadTestsHelper.swift in Sources */, 1E1D8B5D2994FFE100C96994 /* AutoconsentMessageProtocolTests.swift in Sources */, 85C11E532090B23A00BFFEB4 /* UserDefaultsHomeRowReminderStorageTests.swift in Sources */, @@ -8285,6 +8300,7 @@ 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */, B652DF0D287C2A6300C12A9C /* PrivacyFeatures.swift in Sources */, F10E522D1E946F8800CE1253 /* NSAttributedStringExtension.swift in Sources */, + 9F678B892C9BAA4800CA0E19 /* Debouncer.swift in Sources */, 9887DC252354D2AA005C85F5 /* Database.swift in Sources */, F143C3171E4A99D200CFDE3A /* AppURLs.swift in Sources */, C1963863283794A000298D4D /* BookmarksCachingSearch.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4febefbd6a..d8b3ac6978 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" : "cc3629fa16880e410e588a27a6b2426dcc140009", - "version" : "198.0.1" + "revision" : "4db50292abf1180d66da55cf83f75d37395df1f9", + "version" : "198.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "4f0d109f13beec7e8beaf0bd5c0e2c1d528677f8", - "version" : "6.16.0" + "revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1", + "version" : "6.17.0" } }, { diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index 99e16d85cb..8fc63f8e3a 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -44,8 +44,10 @@ protocol ContextualOnboardingLogic { func setSearchMessageSeen() func setFireEducationMessageSeen() + func clearedBrowserData() func setFinalOnboardingDialogSeen() func setPrivacyButtonPulseSeen() + func setDaxDialogDismiss() func canEnableAddFavoriteFlow() -> Bool // Temporary during Contextual Onboarding Experiment func enableAddFavoriteFlow() @@ -227,7 +229,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } private var isNewOnboarding: Bool { - variantManager.isSupported(feature: .newOnboardingIntro) + variantManager.isContextualDaxDialogsEnabled } private var firstBrowsingMessageSeen: Bool { @@ -277,6 +279,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { var isEnabled: Bool { // skip dax dialogs in integration tests guard ProcessInfo.processInfo.environment["DAXDIALOGS"] != "false" else { return false } + guard variantManager.shouldShowDaxDialogs else { return false } return !settings.isDismissed } @@ -317,8 +320,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { func dismiss() { settings.isDismissed = true // Reset last shown dialog as we don't have to show it anymore. - removeLastShownDaxDialog() - removeLastVisitedOnboardingWebsite() + clearOnboardingBrowsingData() } func primeForUse() { @@ -448,11 +450,21 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { saveLastShownDaxDialog(specType: .fire) } + func clearedBrowserData() { + guard isNewOnboarding else { return } + setDaxDialogDismiss() + } + func setPrivacyButtonPulseSeen() { guard isNewOnboarding else { return } settings.privacyButtonPulseShown = true } + func setDaxDialogDismiss() { + guard isNewOnboarding else { return } + clearOnboardingBrowsingData() + } + func setFinalOnboardingDialogSeen() { guard isNewOnboarding else { return } settings.browsingFinalDialogShown = true @@ -543,8 +555,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { saveLastShownDaxDialog(specType: spec.type) saveLastVisitedOnboardingWebsite(url: privacyInfo.url) } else { - removeLastVisitedOnboardingWebsite() - removeLastShownDaxDialog() + clearOnboardingBrowsingData() } return spec @@ -561,6 +572,9 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { } func nextHomeScreenMessageNew() -> HomeScreenSpec? { + // Reset the last browsing information when opening a new tab so loading the previous website won't show again the Dax dialog + clearedBrowserData() + guard let homeScreenSpec = peekNextHomeScreenMessageExperiment() else { currentHomeSpec = nil return nil @@ -592,10 +606,14 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { if nextHomeScreenMessageOverride != nil { return nextHomeScreenMessageOverride } + guard isEnabled else { return nil } + // If the user has already seen the end of journey dialog we don't want to show any other NTP Dax dialog. + guard !finalDaxDialogSeen else { return nil } + // Check final first as if we skip anonymous searches we don't want to show this. - if settings.fireMessageExperimentShown && !finalDaxDialogSeen { + if settings.fireMessageExperimentShown { return .final } @@ -712,6 +730,11 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { return url1.isSameDuckDuckGoSearchURL(other: url2) } + + private func clearOnboardingBrowsingData() { + removeLastShownDaxDialog() + removeLastVisitedOnboardingWebsite() + } } extension URL { diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index 4cffd017af..71bb361a88 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -217,7 +217,7 @@ class HomeViewController: UIViewController, NewTabPage { func openedAsNewTab(allowingKeyboard: Bool) { collectionView.openedAsNewTab(allowingKeyboard: allowingKeyboard) - if !variantManager.isSupported(feature: .newOnboardingIntro) { + if !variantManager.isContextualDaxDialogsEnabled { // In the new onboarding this gets called twice (viewDidAppear in Tab) which then reset the spec to nil. presentNextDaxDialog() } @@ -258,7 +258,9 @@ class HomeViewController: UIViewController, NewTabPage { } func presentNextDaxDialog() { - if variantManager.isSupported(feature: .newOnboardingIntro) { + guard variantManager.shouldShowDaxDialogs else { return } + + if variantManager.isContextualDaxDialogsEnabled { showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) } else { showNextDaxDialog(dialogProvider: newTabDialogTypeProvider) @@ -266,7 +268,7 @@ class HomeViewController: UIViewController, NewTabPage { } func showNextDaxDialog() { - showNextDaxDialog(dialogProvider: newTabDialogTypeProvider) + presentNextDaxDialog() } func reloadFavorites() { diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 324aaf1716..cd05ecddb1 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -35,7 +35,7 @@ extension MainViewController { var controller: (Onboarding & UIViewController)? - if DefaultVariantManager().isSupported(feature: .newOnboardingIntro) { + if DefaultVariantManager().isNewIntroFlow { controller = OnboardingIntroViewController(onboardingPixelReporter: contextualOnboardingPixelReporter) } else { let storyboard = UIStoryboard(name: "DaxOnboarding", bundle: nil) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index dd9e7efeb5..5b1f33e12f 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -852,7 +852,7 @@ class MainViewController: UIViewController { hideNotificationBarIfBrokenSitePromptShown() wakeLazyFireButtonAnimator() - if DefaultVariantManager().isSupported(feature: .newOnboardingIntro) { + if variantManager.isContextualDaxDialogsEnabled { // Dismiss dax dialog and pulse animation when the user taps on the Fire Button. currentTab?.dismissContextualDaxFireDialog() ViewHighlighter.hideAll() @@ -2418,10 +2418,6 @@ extension MainViewController: TabDelegate { } } - func tabDidRequestForgetAll(tab: TabViewController) { - forgetAllWithAnimation(showNextDaxDialog: true) - } - func tabDidRequestFireButtonPulse(tab: TabViewController) { showFireButtonPulse() } @@ -2741,7 +2737,8 @@ extension MainViewController: AutoClearWorker { self.privacyProDataReporter.saveFireCount() // Ideally this should happen once data clearing has finished AND the animation is finished - if showNextDaxDialog { + // `showNextDaxDialog: true` only set from old onboarding FireDialog ActionSheet + if showNextDaxDialog && self.variantManager.shouldShowDaxDialogs { self.homeController?.showNextDaxDialog() } else if KeyboardSettings().onNewTab { let showKeyboardAfterFireButton = DispatchWorkItem { @@ -2751,8 +2748,8 @@ extension MainViewController: AutoClearWorker { self.showKeyboardAfterFireButton = showKeyboardAfterFireButton } - if self.variantManager.isSupported(feature: .newOnboardingIntro) { - DaxDialogs.shared.setFireEducationMessageSeen() + if self.variantManager.isContextualDaxDialogsEnabled { + DaxDialogs.shared.clearedBrowserData() } } } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index fabd1ee3ed..c8ae9157bb 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -165,7 +165,9 @@ final class NewTabPageViewController: UIHostingController some View { configuration.label .fixedSize(horizontal: false, vertical: true) @@ -155,26 +167,13 @@ private struct AddressBarPostionButtonStyle: ButtonStyle { .lineLimit(nil) .padding() .frame(minWidth: 0, maxWidth: .infinity, minHeight: minHeight) - .background(backgroundColor(configuration.isPressed)) + .background(backgroundColor(configuration.isPressed || isSelected)) .cornerRadius(8) .contentShape(Rectangle()) // Makes whole button area tappable, when there's no background } - private func foregroundColor(_ isPressed: Bool) -> Color { - switch (colorScheme, isPressed) { - case (.dark, false): - return .blue30 - case (.dark, true): - return .blue20 - case (_, false): - return .blueBase - case (_, true): - return .blue70 - } - } - - private func backgroundColor(_ isPressed: Bool) -> Color { - switch (colorScheme, isPressed) { + private func backgroundColor(_ isHighlighted: Bool) -> Color { + switch (colorScheme, isHighlighted) { case (.light, true): return .blueBase.opacity(0.2) case (.dark, true): diff --git a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift index 8ff955f2d0..2d1dd5ea4d 100644 --- a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift +++ b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift @@ -34,7 +34,7 @@ struct AppIconPicker: View { @StateObject private var viewModel = AppIconPickerViewModel() - let layout = [GridItem(.adaptive(minimum: Metrics.iconSize, maximum: Metrics.iconSize), spacing: Metrics.spacing)] + let layout = [GridItem(.adaptive(minimum: Metrics.iconSize), spacing: Metrics.spacing, alignment: .leading)] var body: some View { LazyVGrid(columns: layout, spacing: Metrics.spacing) { diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index ea4362b6f1..3a1633086f 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -96,7 +96,7 @@ struct OnboardingFireButtonDialogContent: View { struct OnboardingFirstSearchDoneDialog: View { let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingGotItButton - let message: String + let message: NSAttributedString @State private var showNextScreen: Bool = false @@ -112,7 +112,7 @@ struct OnboardingFirstSearchDoneDialog: View { OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) } else { ContextualDaxDialogContent( - message: NSAttributedString(string: message), + message: message, customActionView: AnyView( OnboardingCTAButton(title: cta) { gotItAction() @@ -248,7 +248,7 @@ struct OnboardingCTAButton: View { } #Preview("First Search Dialog") { - OnboardingFirstSearchDoneDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage, shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) + OnboardingFirstSearchDoneDialog(message: NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage), shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) .padding() } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index 2ca605f244..c1463df1a1 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -114,6 +114,17 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { afterSearchPixelEvent: Pixel.Event, onSizeUpdate: @escaping () -> Void ) -> some View { + + func dialogMessage() -> NSAttributedString { + if onboardingManager.isOnboardingHighlightsEnabled { + let message = UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage + let boldRange = message.range(of: "DuckDuckGo Search") + return message.attributed.with(attribute: .font, value: UIFont.daxBodyBold(), in: boldRange) + } else { + return UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage.attributed + } + } + let viewModel = OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: contextualOnboardingSiteSuggestionsProvider, delegate: delegate, pixelReporter: contextualOnboardingPixelReporter) // If should not show websites search after searching inform the delegate that the user dimissed the dialog, otherwise let the dialog handle it. @@ -129,9 +140,7 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } } - let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage - - return OnboardingFirstSearchDoneDialog(message: message, shouldFollowUp: shouldFollowUpToWebsiteSearch, viewModel: viewModel, gotItAction: gotItAction) + return OnboardingFirstSearchDoneDialog(message: dialogMessage(), shouldFollowUp: shouldFollowUpToWebsiteSearch, viewModel: viewModel, gotItAction: gotItAction) .onFirstAppear { [weak self] in self?.contextualOnboardingPixelReporter.trackScreenImpression(event: afterSearchPixelEvent) } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift index c6d5542b78..fe830d287d 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualOnboardingPresenter.swift @@ -47,7 +47,10 @@ final class ContextualOnboardingPresenter: ContextualOnboardingPresenting { } func presentContextualOnboarding(for spec: DaxDialogs.BrowsingSpec, in vc: TabViewOnboardingDelegate) { - if variantManager.isSupported(feature: .newOnboardingIntro) { + // Extra safety net + guard variantManager.shouldShowDaxDialogs else { return } + + if variantManager.isContextualDaxDialogsEnabled { presentExperimentContextualOnboarding(for: spec, in: vc) } else { presentControlContextualOnboarding(for: spec, in: vc) @@ -55,7 +58,7 @@ final class ContextualOnboardingPresenter: ContextualOnboardingPresenting { } func dismissContextualOnboardingIfNeeded(from vc: TabViewOnboardingDelegate) { - guard variantManager.isSupported(feature: .newOnboardingIntro), let daxContextualOnboarding = vc.daxContextualOnboardingController else { return } + guard variantManager.isContextualDaxDialogsEnabled, let daxContextualOnboarding = vc.daxContextualOnboardingController else { return } remove(daxController: daxContextualOnboarding, fromParent: vc) } diff --git a/DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift b/DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift new file mode 100644 index 0000000000..37909efc26 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/DefaultVariantManager+Onboarding.swift @@ -0,0 +1,43 @@ +// +// DefaultVariantManager+Onboarding.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import BrowserServicesKit + +extension VariantManager { + + var isNewIntroFlow: Bool { + isSupported(feature: .newOnboardingIntro) || isSupported(feature: .newOnboardingIntroHighlights) + } + + var isOnboardingHighlightsExperiment: Bool { + isSupported(feature: .newOnboardingIntroHighlights) + } + + var shouldShowDaxDialogs: Bool { + // Disable Dax Dialogs if only feature supported is .newOnboardingIntro + guard let features = currentVariant?.features else { return true } + return !(features.count == 1 && features.contains(.newOnboardingIntro)) + } + + var isContextualDaxDialogsEnabled: Bool { + isSupported(feature: .contextualDaxDialogs) + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift index a3fc7d14a1..c504061045 100644 --- a/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift +++ b/DuckDuckGo/OnboardingExperiment/Manager/OnboardingManager.swift @@ -32,17 +32,20 @@ protocol OnboardingHighlightsDebugging: OnboardingHighlightsManaging { final class OnboardingManager: OnboardingHighlightsManaging, OnboardingHighlightsDebugging { private var appDefaults: AppDebugSettings private let featureFlagger: FeatureFlagger + private let variantManager: VariantManager init( appDefaults: AppDebugSettings = AppDependencyProvider.shared.appSettings, - featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + variantManager: VariantManager = DefaultVariantManager() ) { self.appDefaults = appDefaults self.featureFlagger = featureFlagger + self.variantManager = variantManager } var isOnboardingHighlightsEnabled: Bool { - isLocalFlagEnabled && isFeatureFlagEnabled + variantManager.isOnboardingHighlightsExperiment || (isLocalFlagEnabled && isFeatureFlagEnabled) } var isLocalFlagEnabled: Bool { diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift index 3d2f8e19bb..a85d4df605 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift @@ -66,7 +66,6 @@ extension OnboardingView { VStack(spacing: 24) { AppIconPicker() - .offset(x: Metrics.pickerLeadingOffset) // Remove left padding for the first item Button(action: action) { Text(UserText.HighlightsOnboardingExperiment.AppIconSelection.cta) diff --git a/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift b/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift index cebcc13db3..8a54b4e306 100644 --- a/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift +++ b/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift @@ -37,7 +37,7 @@ struct OnboardingProgressIndicator: View { VStack(spacing: OnboardingProgressMetrics.verticalSpacing) { HStack { Spacer() - Text("\(stepInfo.currentStep) / \(stepInfo.totalSteps)") + Text(verbatim: "\(stepInfo.currentStep) / \(stepInfo.totalSteps)") .onboardingProgressTitleStyle() .padding(.trailing, OnboardingProgressMetrics.textPadding) } @@ -144,7 +144,7 @@ struct ProgressBarGradient: View { let nextStep = stepInfo.currentStep < stepInfo.totalSteps ? stepInfo.currentStep + 1 : 1 stepInfo = OnboardingProgressIndicator.StepInfo(currentStep: nextStep, totalSteps: stepInfo.totalSteps) }, label: { - Text("Update Progress") + Text(verbatim: "Update Progress") }) } } diff --git a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift index ca4b4fc628..ed71123a3c 100644 --- a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift +++ b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift @@ -21,6 +21,8 @@ import Foundation import Onboarding struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding { + private static let imageSearch = "!image " + private let countryAndLanguageProvider: OnboardingRegionAndLanguageProvider private let onboardingManager: OnboardingHighlightsManaging @@ -68,7 +70,9 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding private var option2: ContextualOnboardingListItem { var search: String - if country == "us" { + // ISO 3166-1 Region capitalized. + // https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html + if isUSCountry { search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption2English } else { search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOption2International @@ -83,9 +87,9 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding private var surpriseMe: ContextualOnboardingListItem { let search = if onboardingManager.isOnboardingHighlightsEnabled { - UserText.HighlightsOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMe + Self.imageSearch + UserText.HighlightsOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMe } else { - country == "us" ? + isUSCountry ? UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish : UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeInternational } @@ -93,4 +97,10 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) } + private var isUSCountry: Bool { + // ISO 3166-1 Region capitalized. + // https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html + country == "US" + } + } diff --git a/DuckDuckGo/TabDelegate.swift b/DuckDuckGo/TabDelegate.swift index 701386d5e6..90424e25fb 100644 --- a/DuckDuckGo/TabDelegate.swift +++ b/DuckDuckGo/TabDelegate.swift @@ -71,8 +71,6 @@ protocol TabDelegate: AnyObject { func tabContentProcessDidTerminate(tab: TabViewController) - func tabDidRequestForgetAll(tab: TabViewController) - func tabDidRequestFireButtonPulse(tab: TabViewController) func tabDidRequestPrivacyDashboardButtonPulse(tab: TabViewController, animated: Bool) diff --git a/DuckDuckGo/TabSwitcherViewController.swift b/DuckDuckGo/TabSwitcherViewController.swift index 8d41d58603..9a6aafd7d8 100644 --- a/DuckDuckGo/TabSwitcherViewController.swift +++ b/DuckDuckGo/TabSwitcherViewController.swift @@ -317,8 +317,27 @@ class TabSwitcherViewController: UIViewController { } @IBAction func onFirePressed(sender: AnyObject) { + + func presentForgetDataAlert() { + let alert = ForgetDataAlert.buildAlert(forgetTabsAndDataHandler: { [weak self] in + self?.forgetAll() + }) + + if let anchor = sender as? UIView { + self.present(controller: alert, fromView: anchor) + } else { + self.present(controller: alert, fromView: toolbar) + } + } + Pixel.fire(pixel: .forgetAllPressedTabSwitching) - let isNewOnboarding = DefaultVariantManager().isSupported(feature: .newOnboardingIntro) + let variantManager = DefaultVariantManager() + let isNewOnboarding = variantManager.isContextualDaxDialogsEnabled + + guard variantManager.shouldShowDaxDialogs else { + presentForgetDataAlert() + return + } if !isNewOnboarding && DaxDialogs.shared.shouldShowFireButtonPulse { @@ -328,15 +347,7 @@ class TabSwitcherViewController: UIViewController { if isNewOnboarding { ViewHighlighter.hideAll() } - let alert = ForgetDataAlert.buildAlert(forgetTabsAndDataHandler: { [weak self] in - self?.forgetAll() - }) - - if let anchor = sender as? UIView { - self.present(controller: alert, fromView: anchor) - } else { - self.present(controller: alert, fromView: toolbar) - } + presentForgetDataAlert() } } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 514b8931de..668ff0364a 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -186,6 +186,8 @@ class TabViewController: UIViewController { let syncService: DDGSyncing + private let daxDialogsDebouncer = Debouncer(mode: .common) + public var url: URL? { willSet { if newValue != url { @@ -985,7 +987,13 @@ class TabViewController: UIViewController { webView.scrollView.refreshControl = isEnabled ? refreshControl : nil } - private var didGoBackForward: Bool = false + private var didGoBackForward: Bool = false { + didSet { + if didGoBackForward { + contextualOnboardingPresenter.dismissContextualOnboardingIfNeeded(from: self) + } + } + } private func resetDashboardInfo() { if let url = url { @@ -1447,7 +1455,11 @@ extension TabViewController: WKNavigationDelegate { tabModel.link = link delegate?.tabLoadingStateDidChange(tab: self) - showDaxDialogOrStartTrackerNetworksAnimationIfNeeded() + // Present the Dax dialog with a delay to mitigate issue where user script detec trackers after the dialog is show to the user + // Debounce to avoid showing multiple animations on redirects. e.g. !image baby ducklings + daxDialogsDebouncer.debounce(for: 0.8) { [weak self] in + self?.showDaxDialogOrStartTrackerNetworksAnimationIfNeeded() + } Task { @MainActor in if await webView.isCurrentSiteReferredFromDuckDuckGo { @@ -1496,7 +1508,7 @@ extension TabViewController: WKNavigationDelegate { return } - if !DefaultVariantManager().isSupported(feature: .newOnboardingIntro) { + if !DefaultVariantManager().isContextualDaxDialogsEnabled { isShowingFullScreenDaxDialog = true } scheduleTrackerNetworksAnimation(collapsing: !spec.highlightAddressBar) @@ -3016,6 +3028,9 @@ extension TabViewController: ContextualOnboardingEventDelegate { } func didTapDismissContextualOnboardingAction() { + // Reset last visited onboarding site and last dax dialog shown. + contextualOnboardingLogic.setDaxDialogDismiss() + contextualOnboardingPresenter.dismissContextualOnboardingIfNeeded(from: self) } diff --git a/DuckDuckGo/TabsBarViewController.swift b/DuckDuckGo/TabsBarViewController.swift index 317efa98a9..2de9144342 100644 --- a/DuckDuckGo/TabsBarViewController.swift +++ b/DuckDuckGo/TabsBarViewController.swift @@ -104,7 +104,14 @@ class TabsBarViewController: UIViewController { self.present(controller: alert, fromView: fireButton) } - if DefaultVariantManager().isSupported(feature: .newOnboardingIntro) { + let variantManager = DefaultVariantManager() + + guard variantManager.shouldShowDaxDialogs else { + showClearDataAlert() + return + } + + if variantManager.isContextualDaxDialogsEnabled { delegate?.tabsBarDidRequestFireEducationDialog(self) showClearDataAlert() } else { @@ -322,7 +329,7 @@ extension MainViewController: TabsBarDelegate { } func tabsBarDidRequestFireEducationDialog(_ controller: TabsBarViewController) { - if DefaultVariantManager().isSupported(feature: .newOnboardingIntro) { + if DefaultVariantManager().isContextualDaxDialogsEnabled { currentTab?.dismissContextualDaxFireDialog() ViewHighlighter.hideAll() } else { diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index c8c2fe0910..d273f025ce 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1393,7 +1393,7 @@ But if you *do* want a peek under the hood, you can find more information about enum AddressBarPosition { public static let title = NSLocalizedString("onboarding.highlights.addressBarPosition.title", value: "Where should I put your address bar?", comment: "The title of the onboarding dialog popup to select the preferred address bar position.") public static let topTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.top.title", value: "Top", comment: "The title of the option to set the address bar to the top.") - public static let defaultOption = NSLocalizedString("onboarding.highlights.addressBarPosition.default", value: "(Default)", comment: "Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default)") + public static let defaultOption = NSLocalizedString("onboarding.highlights.addressBarPosition.default", value: "(default)", comment: "Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default)") public static let topMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.top.message", value: "Easy to see", comment: "The message of the option to set the address bar to the top.") public static let bottomTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.title", value: "Bottom", comment: "The title of the option to set the address bar to the bottom.") public static let bottomMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.message", value: "Easy to reach", comment: "The message of the option to set the address bar to the bottom.") diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index 28f226f39d..080d97d50c 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Разбрах!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Запомнете: Всеки път, когато сърфирате с мен, аз ще подрязвам крилцата на досадните реклами."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Това е DuckDuckGo Search! Поверителен. Бърз. По-малко реклами."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Търсенето в DuckDuckGo винаги е поверително."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "патенца"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Опитайте да посетите сайт!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Изберете своя браузър"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Блокиране на изскачащи прозорци за бисквитки"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Блокиране на досадни реклами"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Бързо изтриване на данните за сърфиране"; /* Message to highlight browser capability of private searches */ @@ -1792,6 +1804,56 @@ /* Subheader message for the screen to choose DuckDuckGo as default browser */ "onboarding.defaultBrowser.message" = "Отваряйте връзки без притеснения, всеки път."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "По-лесен достъп"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Отдолу"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Следващ"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(по подразбиране)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Къде да сложа адресната лента?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "По-добре се вижда"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Най-горе"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Следващ"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Изберете икона за приложенията:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Кой цвят изглежда най-добре на този фон?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Блокиране на заявки и изскачащи прозорци за бисквитки"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Блокиране на целеви реклами"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Бързо изтриване на данни за сърфиране"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Блокиране на тракерите на трети страни"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Защитите са активирани!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Пропускане"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Здравейте.\n\nГотови ли сте за по-бърз браузър, който осигурява защита?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Да го направим!"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index 089d695f78..b71418844a 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Mám to!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Pamatujte: pokaždé, když internet procházíte s námi, příšerným reklamám přistřihneme křídla."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "To je vyhledávač DuckDuckGo Search! Soukromý. Rychlý. S méně reklamami."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Tvoje vyhledávání v DuckDuckGo je vždycky soukromé."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "malá káčátka"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Zkus přejít na nějaký web!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Vyber si prohlížeč"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokování vyskakovacích oken ohledně cookies"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokování reklam, které tě všude pronásledují"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Rychle vymaž údaje o procházení"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Ochrana osobních údajů je aktivní!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Otevírejte odkazy s klidem mysli, pokaždé."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Vždy po ruce"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Dole"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Další"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(výchozí)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Kam mám umístit váš adresní řádek?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Snadno viditelné"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Nahoru"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Další"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Vyberte si ikonu aplikace:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Která barva mi sluší nejvíc?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokování požadavků na soubory cookie a vyskakovacích oken"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokování cílených reklam"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Rychle vymazání údajů o prohlížení"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokovat trackery třetích stran"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Ochrana je aktivní!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Přeskočit"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Dobrý den.\n\nJsi připravený na rychlejší prohlížeč, který tě ochrání?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Pojďme na to!"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index b4a5cdd89d..299a8aa8b1 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Forstået"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Husk: hver gang du browser med mig, mister en uhyggelig annonce sine vinger."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Det er DuckDuckGo Search! Privat. Hurtig. Færre annoncer."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Dine DuckDuckGo-søgninger er altid private."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "ællinger"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Prøv at besøge en hjemmeside!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Vælg din browser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Bloker pop op-vinduer om cookies"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Bloker uhyggelige annoncer"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Ryd hurtigt browserdata"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Beskyttelse af privatlivet er aktiveret!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Åbne link med ro i sindet, hver gang."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Let at komme til"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Nederst"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Næste"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(standard)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Hvor skal jeg placere din adresselinje?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Let at se"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Top"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Næste"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Vælg dit app-ikon:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Hvilken farve klæder mig bedst?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Bloker cookie-anmodninger og popups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Bloker målrettede annoncer"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Slet browserdata hurtigt"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Bloker tredjeparts-trackere"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Beskyttelse aktiveret!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Spring over"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hej\n\nEr du klar til en hurtigere browser, der holder dig beskyttet?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Lad os gøre det!"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 0b04f1dfcb..4368d10bc8 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Verstanden."; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Hinweis: Jedes Mal, wenn du mit mir browst, verliert eine gruselige Anzeige ihren Schrecken."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Das ist die DuckDuckGo Search! Privat. Schnell. Weniger Werbung."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Deine Suchen über DuckDuckGo sind immer privat."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "Entenküken"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Versuche, eine Website zu besuchen!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Wähle deinen Browser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Cookie-Pop-ups blockieren"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Aufdringliche Werbung blockieren"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Browserdaten schnell löschen"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Datenschutz aktiviert!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Öffne Links jederzeit sicher und ohne Sorge."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Leicht erreichbar"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Unten"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Weiter"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(standard)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Wo soll deine Adressleiste platziert werden?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Leicht erkennbar"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Nach oben"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Weiter"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Wähle dein App-Symbol aus:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Welche Farbe gefällt dir am besten?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Cookie-Anfragen und Popups blocken"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Gezielte Werbung blockieren"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Browserdaten schnell löschen"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blockiert Tracker von Drittanbietern"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Schutz aktiviert!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Überspringen"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hallo.\n\nBereit für einen schnelleren Browser, der dich schützt?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Los geht's!"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 6606ee33a3..8e74f39767 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Το κατάλαβα!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Να θυμάστε: κάθε φορά που περιηγείστε μαζί μου, μια ανατριχιαστική διαφήμιση χάνει τη δύναμή της!"; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Αυτό είναι το DuckDuckGo Search! Ιδιωτικά. Γρήγορα. Λιγότερες διαφημίσεις."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Οι αναζητήσεις σας στο DuckDuckGo είναι πάντα ιδιωτικές."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "παπάκια"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Δοκιμάστε να επισκεφτείτε έναν ιστότοπο!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Επιλέξτε το πρόγραμμα περιήγησής σας"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Αποκλεισμός αναδυόμενων παραθύρων cookie"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Αποκλεισμός στοχευμένων διαφημίσεων"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Γρήγορη διαγραφή δεδομένων περιήγησης"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "H προστασία προσωπικών δεδομένων energopoi;huhke!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Ανοίξτε συνδέσμους με ηρεμία και ασφάλεια, κάθε φορά."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Εύκολη πρόσβαση"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Κάτω μέρος"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Επόμενο"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(προεπιλογή)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Πού πρέπει να τοποθετήσω τη γραμμή διευθύνσεών σας;"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Ευδιάκριτο"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Κορυφή"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Επόμενο"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Επιλέξτε το εικονίδιο της εφαρμογής σας:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Ποιο χρώμα μου ταιριάζει καλύτερα;"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Αποκλεισμός αιτημάτων και αναδυόμενων παραθύρων για cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Αποκλεισμός στοχευμένων διαφημίσεων"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Γρήγορη διαγραφή δεδομένων περιήγησης"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Αποκλείστε εφαρμογές παρακολούθησης τρίτων"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Η προστασία ενεργοποιήθηκε!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Παράλειψη"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Γεια σας.\n\nΕίστε έτοιμοι για ένα ταχύτερο πρόγραμμα περιήγησης που σας διατηρεί προστατευμένους;"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Ας το δοκιμάσουμε!"; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 2d4b428f71..dfa29f18db 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1872,7 +1872,7 @@ https://duckduckgo.com/mac"; "onboarding.highlights.addressBarPosition.cta" = "Next"; /* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ -"onboarding.highlights.addressBarPosition.default" = "(Default)"; +"onboarding.highlights.addressBarPosition.default" = "(default)"; /* The title of the onboarding dialog popup to select the preferred address bar position. */ "onboarding.highlights.addressBarPosition.title" = "Where should I put your address bar?"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 1b75d2cac7..31d5b5ee75 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Entendido"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Recuerda: cada vez que navegas conmigo corto las alas a un anuncio horrible."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "¡Eso es DuckDuckGo Search! Privado. Rápido. Menos anuncios."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Tus búsquedas en DuckDuckGo son siempre privadas."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "patitos"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "¡Intenta visitar un sitio!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Elige tu navegador"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Bloqueo de ventanas emergentes de cookies"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Bloqueo de anuncios escalofriantes"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Borra rápidamente los datos de navegación"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "¡Protecciones de privacidad activadas!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Abre los enlaces con tranquilidad, siempre."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Fácil de alcanzar"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Inferior"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Siguiente"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(predeterminado)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "¿Dónde debo poner tu barra de direcciones?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Fácil de ver"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Arriba"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Siguiente"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Elige el icono de tu aplicación:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "¿Qué color me queda mejor?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Bloqueo de las solicitudes de cookies y las ventanas emergentes"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Bloqueo de anuncios segmentados"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Borra rápidamente los datos de navegación"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Bloquea rastreadores de terceros"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "¡Protecciones activadas!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Omitir"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hola.\n\n¿Todo listo para un navegador más rápido que mantiene tu protección?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "¡Adelante!"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index 1e72c63aec..bebcd22c02 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Sain aru!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Pea meeles: iga kord kui minuga sirvid, kaotab jube reklaam oma tiivad."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "See on DuckDuckGo Search! Privaatne. Kiire. Vähem reklaame."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Sinu DuckDuckGo otsingud on alati privaatsed."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "pardipojad"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Proovi külastada saiti!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Vali oma brauser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokeeri küpsiste hüpikaknad"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokeeri sihitud reklaamid"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Sirvimisandmete kiire kustutamine"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Privaatsuskaitsed on aktiveeritud!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Ava linke igal ajal, rahuliku meelega."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Kergesti ligipääsetav"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "All"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Järgmine"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(vaikimisi)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Kuhu soovid oma aadressiriba paigutada?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Kergesti märgatav"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Populaarseimad"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Järgmine"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Vali oma rakenduse ikoon:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Milline värv tundub kõige paremini sobivat?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokeeri küpsiste päringud ja hüpikaknad"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokeeri suunatud reklaamid"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Kustuta sirvimisandmed kiiresti"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokeeri kolmanda poole jälgurid"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Kaitsed aktiveeritud!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Jäta vahele"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Tere!\n\nKas oled valmis kasutama kiiremat brauserit, mis hoiab sind kaitstud?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Teeme ära!"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index f3689a274a..376b58b096 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Selvä!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Muista, että joka kerta kun käytät minua selaamiseen, rasittavat mainokset katoavat."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Tämä on DuckDuckGo Search! Yksityinen. Nopea. Vähemmän mainoksia."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "DuckDuckGo-hakusi ovat aina yksityisiä."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "ankanpoikaset"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Siirry sivustolle!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Valitse selaimesi"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Estä evästeponnahdusikkunat"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Estä rasittavat mainokset"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Tyhjennä selaustiedot nopeasti"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Yksityisyyden suoja aktivoitu!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Avaa linkit ilman stressiä, joka kerta."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Helppo tavoittaa"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Alareuna"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Seuraava"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(oletus)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Mihin osoitepalkki pitäisi sijoittaa?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Helppo nähdä"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Suosituin"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Seuraava"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Valitse sovelluskuvake:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Mikä väri sopii minulle parhaiten?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Estä evästepyynnöt ja ponnahdusikkunat"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Estä kohdennetut mainokset"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Poista selaustiedot nopeasti"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Estä kolmannen osapuolen seuraimet"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Suojaukset aktivoitu!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Ohita"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hei!\n\nOletko valmis käyttämään nopeampaa selainta, joka pitää sinut suojattuna?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Aloitetaan!"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index 5eaba8d70c..8d4427bb5a 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "J'ai compris !"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Souvenez-vous : chaque fois que vous naviguez avec moi, des publicités intrusives disparaissent."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "C'est DuckDuckGo Search ! Privé. Rapide. Moins de publicités."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Vos recherches sur DuckDuckGo restent confidentielles."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "bébés canetons"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Essayez de visiter un site !"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Choisissez votre navigateur"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Bloquez les fenêtres contextuelles de cookies"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Bloquez les publicités douteuses"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Effacer rapidement les données de navigation"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Protections de la confidentialité activées !"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Des liens ouverts en toute tranquillité d'esprit, à chaque fois."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Facile d'accès"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "En bas"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Suivant"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(par défaut)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Où dois-je placer votre barre d'adresse ?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Facile à voir"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Haut de page"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Suivant"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Choisissez l'icône de votre application :"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Quelle couleur me va le mieux ?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Bloquez les demandes et les fenêtres contextuelles de cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Bloquez les publicités ciblées"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Effacez rapidement les données de navigation"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Bloquer les traqueurs tiers"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Protections activées !"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Ignorer"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Bonjour.\n\nEnvie de profiter d'un navigateur plus rapide qui vous protège ?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "C'est parti !"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index e762b4d00f..c100cd7ce3 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Shvaćam!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Zapamti: svaki put kada pregledavaš sa mnom grozna reklama gubi krila."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "To je DuckDuckGo Search! Privatno. Brzo. Manje oglasa."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Tvoja su DuckDuckGo pretraživanja uvijek privatna."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "pačići"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Pokušaj posjetiti web-mjesto!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Odaberi preglednik"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokiraj skočne prozore kolačića"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokiraj neželjene oglase"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Brzo izbriši podatke pregledavanja"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Zaštita privatnosti aktivirana!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Svaki put poveznice otvori spokojno."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Lako dostupno"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Dno"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Dalje"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(zadano)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Gdje trebam staviti tvoju adresnu traku?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Lako se vidi"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Vrh"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Dalje"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Odaberi ikonu aplikacije:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Koja boja izgleda najbolje na meni?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokiraj zahtjeve kolačića & skočne prozore"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokiraj ciljane oglase"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Brzo izbriši podatke o pregledavanju"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokiraj alate za praćenje trećih strana"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Zaštite aktivirane!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Preskoči"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Pozdrav.\n\nJesi li spreman za brži preglednik koji te štiti?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Učinimo to!"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index c6a6b91164..fbda3e6e8e 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Megvan!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Ne feledd: minden alkalommal, amikor velem böngészel, egy undok hirdetés elveszíti az erejét."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Ez a DuckDuckGo Search! Privát. Gyors. Kevesebb hirdetés."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "A DuckDuckGo-kereséseid mindig privátak maradnak."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "kiskacsák"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Próbálj meg ellátogatni egy webhelyre!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Böngésző kiválasztása"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Felugró sütiablakok blokkolása"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Gyanús hirdetések blokkolása"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Böngészési adatok gyors törlése"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Adatvédelem aktiválva!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Nyugodtan kattints a linkekre, mindig."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Könnyen elérhető"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Alul"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Következő"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(alapértelmezett)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Hol helyezzem el a címsort?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Könnyen látható"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Fel"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Következő"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Válaszd ki az alkalmazás ikonját:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Melyik szín áll nekem a legjobban?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Sütikérelmek és felugró ablakok blokkolása"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Célzott hirdetések blokkolása"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Böngészési adatok gyors törlése"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Harmadik féltől származó nyomkövetők blokkolása"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Védelem aktiválva!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Kihagyás"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Üdv!\n\nFelkészültél egy gyorsabb böngészőre, amely védelmet nyújt neked?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Rajta, csináljuk!"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index 4f14d0d1d1..965c08a037 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Ho capito!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Ricorda: quando navighi con me gli annunci inquietanti non possono seguirti."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "È DuckDuckGo Search! Privato. Veloce. Meno annunci."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Le tue ricerche su DuckDuckGo sono sempre private."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "anatroccoli"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Prova a visitare un sito!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Scegli il tuo browser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blocca i popup dei cookie"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blocca gli annunci invasivi"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Elimina rapidamente i dati di navigazione"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Protezioni della privacy attivate!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Apri i collegamenti con la massima tranquillità, ogni volta."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Facile da raggiungere"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Parte inferiore"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Successivo"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(predefinito)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Dove devo mettere la barra degli indirizzi?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Facile da vedere"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Parte superiore"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Successivo"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Scegli l'icona della tua app:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Quale colore mi sta meglio?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blocca le richieste di cookie e i popup"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blocca gli annunci mirati"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Cancella rapidamente i dati di navigazione"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blocca i sistemi di tracciamento di terze parti"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Protezioni attivate!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Salta"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Ciao.\n\nVuoi un browser più veloce che ti garantisca protezione?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Continua"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index cc7383f995..772a8f8c00 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Supratau!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Įsidėmėk: kiekvieną kartą, kai naršai su manimi, bauginantis skelbimas praranda galią."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Tai – „DuckDuckGo Search“! Privati. Sparti. Mažiau reklamų."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Jūsų „DuckDuckGo“ paieškos visada yra privačios."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "kūdikių ančiukai"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Pabandyk apsilankyti svetainėje!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Pasirinkite naršyklę"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokuoti slapukų iššokančiuosius langus"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokuokite taikomus skelbimus"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Greitai ištrinkite naršymo duomenis"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Privatumo apsaugos priemonės įjungtos!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Kiekvieną kartą be jaudulio atverkite nuorodas."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Lengvai pasiekiama"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Apačia"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Kitas"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(numatytasis)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Kur turėčiau nustatyti jūsų adreso juostą?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Lengva pamatyti"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Viršus"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Kitas"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Pasirinkti programos piktogramą:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Kuri spalva man labiausiai tinka?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokuoti slapukų užklausas ir iššokančiuosius langus"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokuoti tikslines reklamas"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Greitai ištrinkite naršymo duomenis"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokuoja trečiųjų šalių stebėjimo priemones"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Apsaugos priemonės įjungtos!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Praleisti"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Sveiki.\n\nPasiruošę greitesnei naršyklei, kuri jus apsaugo?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Pirmyn!"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 6aa8661114..1332abd838 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Sapratu!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Atceries: katru reizi, kad pārlūkosi kopā ar mani, kaitinošās reklāmas zaudēs savu spēku!"; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Tā ir DuckDuckGo Search meklēšana! Privāti. Ātri. Mazāk reklāmu."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Tavi DuckDuckGo meklējumi vienmēr ir privāti."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "mazi pīlēni"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Pamēģini apmeklēt kādu vietni!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Izvēlies pārlūku"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Bloķē sīkfailu uznirstošos logus"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Bloķē kaitinošas reklāmas"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Ātri izdzēs pārlūkošanas datus"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Privātuma aizsardzība ir aktivizēta!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Katru reizi atver saites bez raizēm."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Viegli pieejama"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Apakšā"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Nākamais"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(noklusējuma)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Kur novietot adreses joslu?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Viegli pamanāma"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Populārākie"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Nākamais"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Izvēlies lietotnes ikonu:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Kura krāsa man ir vispiemērotākā?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Bloķē sīkfailu pieprasījumus un uznirstošos logus"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Bloķē mērķtiecīgas reklāmas"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Ātri izdzēs pārlūkošanas datus"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Bloķē trešo pušu izsekotājus"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Aizsardzība ir aktivizēta!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Izlaist"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Sveiki!\n\nVai esi gatavs ātrākai pārlūkprogrammai, kas tevi aizsargā?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Aiziet!"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index 21b148b717..f36f0fb376 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Skjønner!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Husk: Hver gang du surfer med meg, klippes vingene på en uhyggelig annonse."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Det er DuckDuckGo Search! Privat. Raskt. Færre annonser."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "DuckDuckGo-søkene dine er alltid private."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "andunger"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Prøv å besøke et nettsted!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Velg nettleseren din"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokker popup-vinduer om informasjonskapsler"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokker påtrengende annonser"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Slett nettleserdata raskt"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Personvern er aktivert!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Åpne lenker med ro i sinnet, hver gang."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Lett å nå"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Nederst"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Neste"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(standard)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Hvor skal jeg plassere adressefeltet?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Lett å se"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Topp"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Neste"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Velg appikonet ditt:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Hvilken farge passer best på meg?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokker forespørsler om informasjonskapsler og popup-vinduer"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokker målrettede annonser"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Slett nettleserdata raskt"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokker tredjepartssporere"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Beskyttelser er aktivert!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Hopp over"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Heisann.\n\nKlar for en raskere nettleser som holder deg beskyttet?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Sett i gang!"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index 3bad0bbe3b..3b91d4e02a 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Ik snap het!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Denk eraan: elke keer als je met mij browset, verliest een enge advertentie zijn vleugels."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Dat is DuckDuckGo Search! Privé. Snel. Minder advertenties."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Je DuckDuckGo-zoekopdrachten zijn altijd privé."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "baby-eendjes"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Bezoek eens een site!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Kies je browser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokkeer cookiepop-ups"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokkeer enge advertenties"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Wis browsegegevens in een handomdraai"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Privacybescherming geactiveerd!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Open links altijd met een gerust hart."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Gemakkelijk te bereiken"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Onderkant"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Volgende"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(standaard)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Waar moet ik je adresbalk plaatsen?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Gemakkelijk te zien"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Boven"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Volgende"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Kies het app-pictogram:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Welke kleur staat mij het beste?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokkeer cookieverzoeken en pop-ups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokkeer gerichte advertenties"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Wis browsegegevens snel"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokkeer trackers van derden"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Bescherming geactiveerd!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Overslaan"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hoi.\n\nKlaar voor een snellere browser die je beschermt?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Laten we het doen!"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 24176d0cfd..365501da12 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Rozumiem!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Pamiętaj: za każdym razem, gdy przeglądasz ze mną Internet, jakaś wścibska reklama przestaje działać."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "To DuckDuckGo Search! Prywatna. Szybka. Z mniejszą liczbą reklam."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Wyszukiwania w DuckDuckGo zawsze są prywatne."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "małe kaczuszki"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Spróbuj odwiedzić witrynę!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Wybierz przeglądarkę"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokuj wyskakujące okienka z informacją o plikach cookie"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokuj wścibskie reklamy"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Szybko usuwaj dane przeglądania"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Aktywowano ochronę prywatności!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Zawsze otwieraj linki ze spokojem."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Łatwy dostęp"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Dół"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Dalej"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(domyślne)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Gdzie umieścić pasek adresu?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Łatwy dostęp"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Do góry"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Dalej"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Wybierz ikonę aplikacji:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Który kolor będzie najlepszy?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokowanie wyskakujących okienek z informacją o plikach cookie"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokowanie ukierunkowanych reklam"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Szybkie usuwanie danych przeglądania"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokuj mechanizmy śledzące innych firm"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Ochrona aktywowana!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Pomiń"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Cześć!\n\nChcesz skorzystać z szybszej przeglądarki, która zapewni Ci ochronę?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Zróbmy to!"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index 808a4577a5..2fe4c8d1e2 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Entendi!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Lembre-se: sempre que navega comigo, um anúncio estranho perde as suas asas."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "É o DuckDuckGo Search! Privado. Rápido. Menos anúncios."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "As tuas pesquisas no DuckDuckGo são sempre privadas."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "patinhos bebés"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Experimenta visitar um site!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Escolhe o teu navegador"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Bloquear pop-ups de cookies"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Bloquear anúncios assustadores"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Apaga rapidamente os dados de navegação"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Proteções de privacidade ativadas!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Abra links com tranquilidade, sempre."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Fácil de aceder"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Parte inferior"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Seguinte"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(predefinido)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Onde devo posicionar a barra de endereços?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Fácil de ver"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Topo"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Seguinte"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Escolhe o ícone da aplicação:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Que cor me fica melhor?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Bloquear pedidos de cookies e pop-ups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Bloquear anúncios segmentados"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Eliminar dados de navegação rapidamente"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Bloqueia rastreadores de terceiros"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Proteções ativadas!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Ignorar"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Olá.\n\nPronto para um navegador mais rápido que te mantém protegido?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Vamos lá!"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index e544c45d0a..19f6124d9f 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Am înțeles!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Reține: de fiecare dată când navighezi cu mine, o reclamă terifiantă își pierde aripile."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Acesta este DuckDuckGo Search! Privat. Rapid. Mai puține reclame."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Căutările tale DuckDuckGo sunt întotdeauna private."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "rățuște"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Încearcă să vizitezi un site!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Alege browserul"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blochează ferestrele pop-up privind modulele cookie"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blochează reclamele înfiorătoare"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Șterge rapid datele de navigare"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Măsurile de protecție a confidențialității au fost activate!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Deschide link-urile liniștit, de fiecare dată."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Ușor de accesat"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Partea de jos"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Următorul"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(implicit)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Unde ar trebui să îți pun bara de adrese?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Ușor de văzut"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Sus"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Următorul"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Alege pictograma aplicației:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Care culoare arată cel mai bine pentru mine?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blochează solicitările modulelor cookie și ferestrele pop-up"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blochează reclamele direcționate"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Șterge rapid datele de navigare"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blochează tehnologiile de urmărire externe"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Mecanismele de protecție au fost activate!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Ignorare"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Salut!\n\nEști gata pentru un browser mai rapid care îți asigură protecția?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Să începem!"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 082fc1c61a..18fe1e3c3b 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Понятно"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Бродить по сайтам с нами — значит подрезать крылья назойливой рекламе."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Это — DuckDuckGo Search. Надежно. Быстро. Меньше рекламы."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Ваши поисковые запросы в DuckDuckGo всегда конфиденциальны."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "утята"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Попробуйте посетить сайт!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Выбрать браузер"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Блокировка всплывающих окон куки"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Защита от надоедливой рекламы"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Быстрая очистка данных из браузера"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Защита конфиденциальности уже включена!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Открывайте ссылки, ни о чем не беспокоясь."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Легко использовать"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Внизу"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Далее"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(по умолчанию)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Где следует разместить адресную строку?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Легко заметить"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Вверх"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Далее"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Выберите иконку приложения:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Какой цвет мне идет?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Блокировка запросов и всплывающих окон куки"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Блокировка целевой рекламы"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Быстрое удаление данных из браузера"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Блокировка сторонних трекеров"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Защита включена"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Пропустить"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Привет!\n\nБыстрый и надежный браузер заказывали?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Поехали!"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 7d3fc58141..c81d9cefc8 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Rozumiem!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Pamätajte: zakaždým, keď prehliadate v našej aplikácii, divnej reklame pristrihávate krídla."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "To je DuckDuckGo Search! Súkromne. Rýchlo. Menej reklám."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Vaše vyhľadávania v službe DuckDuckGo sú vždy súkromné."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "malé kačiatka"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Skúste navštíviť stránku!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Vyberte si prehliadač"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokovanie vyskakovacích okien o súboroch cookie"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokovať nepríjemné reklamy"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Rýchle vymazanie údajov prehliadania"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Ochrana súkromia bola aktivovaná!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Vždy otvor odkazy s pokojom mysle."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Ľahko dosiahnuteľné"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Spodná časť"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Ďalšie"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(predvolené)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Kam mám umiestniť váš riadok s adresou?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Ľahko viditeľné"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Hore"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Ďalšie"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Vyberte ikonu aplikácie:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Ktorá farba na mne vyzerá najlepšie?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokovanie požiadaviek na súbory cookie & vyskakovacie okná"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokovanie cielených reklám"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Rýchlo vymažte údaje prehliadania"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokovať sledovanie tretími stranami"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Ochrany boli aktivované!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Preskočiť"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Dobrý deň.\n\nSte pripravení na rýchlejší prehliadač, ktorý vás ochráni?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Poďme na to!"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 39f89204b9..7ba7bc5a30 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Razumem!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Ne pozabi: vsakič, ko brskate z mano, grozljiv oglas izgubi krila."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "To je iskanje DuckDuckGo Search! Zasebno. Hitro. Z manj oglasi."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Vaša iskanja v DuckDuckGo so vedno zasebna."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "račke"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Poskusite obiskati spletno stran!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Izberite brskalnik"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blokirajte pojavna okna za piškotke"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blokirajte srhljive oglase"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Hitro izbrišite podatke brskanja"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Zaščite zasebnosti so aktivirane!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Bodite pomirjeni, ko odpirate povezave."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Preprosto dosegljivo"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Spodaj"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Naslednji"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(privzeto)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Kam naj postavim vašo naslovno vrstico?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Preprosto vidno"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Vrh"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Naslednji"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Izberite ikono aplikacije:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Katera barva mi najbolje pristaja?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blokiranje zahtev za piškotke in pojavna okna"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blokiranje ciljnih oglasov"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Hitro izbrišite podatke o brskanju"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blokirajte sledilnike tretjih oseb"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Zaščite so aktivirane!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Preskoči"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Pozdravljeni!\n\nSte pripravljeni na hitrejši brskalnik, ki vas zaščiti?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Pa začnimo!"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index 5385fcc294..8db82189fc 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Jag förstår!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Kom ihåg: varje gång du surfar med mig förlorar en läskig annons sina vingar."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "Det är DuckDuckGo Search! Privat. Snabbt. Färre annonser."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Dina DuckDuckGo-sökningar är alltid privata."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "ankungar"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Prova att besöka en webbplats!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Välj din webbläsare"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Blockera popup-fönster för cookies"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Blockera påträngande annonser"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Rensa snabbt surfdata"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Integritetsskydd aktiverat!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Sluta oroa dig varje gång du öppnar länkar."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Lätt att nå"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Botten"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Nästa"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(standard)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Var ska jag placera adressfältet?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Lätt att se"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Topp"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Nästa"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Välj appikon:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Vilken färg passar bäst på mig?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Blockera förfrågningar och popup-fönster för cookies"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Blockera riktade annonser"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Radera webbläsardata snabbt"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Blockera spårare från tredje part"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Skydd aktiverat!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Hoppa över"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hej!\n\nÄr du redo för en snabbare webbläsare som också skyddar dig?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Vi kör!"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index e9453b557e..b74d0424a9 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -785,6 +785,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Anladım!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Unutmayın: İnterneti benimle ne kadar çok gezerseniz rahatsız edici reklamları da o kadar az görürsünüz."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "DuckDuckGo Search İşte Bu! Gizli. Hızlı. Daha az reklam."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "DuckDuckGo aramalarınız her zaman gizlidir."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "ördek yavruları"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Bir siteyi ziyaret etmeyi deneyin!"; @@ -1772,13 +1784,13 @@ /* Button to change the default browser */ "onboarding.browsers.cta" = "Tarayıcınızı Seçin"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Çerez açılır pencerelerini engelle"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Tekinsiz reklamları engelleyin"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Tarama verilerini hızlıca silin"; /* Message to highlight browser capability of private searches */ @@ -1790,8 +1802,56 @@ /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Gizlilik korumaları etkinleştirildi!"; -/* Subheader message for the screen to choose DuckDuckGo as default browser */ -"onboarding.defaultBrowser.message" = "Her zaman gönül rahatlığıyla açık bağlantılar."; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Ulaşması kolay"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Alt"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Sonraki"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(varsayılan)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Adres çubuğu nereye yerleştirilsin?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Görmesi kolay"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Top"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Sonraki"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Uygulama simgenizi seçin:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Bana hangi renk daha çok yakışıyor?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Çerez isteklerini ve açılır pencereleri engelleyin"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Hedefli reklamları engelleyin"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Tarama verilerini hızlıca silin"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Üçüncü taraf izleyicileri engelleyin"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Korumalar etkinleştirildi!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Atla"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Merhaba.\n\nSizi koruyan daha hızlı bir tarayıcıya hazır mısınız?"; /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Hadi Başlayalım!"; diff --git a/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift b/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift index ff95a620aa..83789d29b7 100644 --- a/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift +++ b/DuckDuckGoTests/ContextualOnboardingPresenterTests.swift @@ -35,11 +35,11 @@ final class ContextualOnboardingPresenterTests: XCTestCase { } - func testWhenPresentContextualOnboardingAndVariantDoesNotSupportOnboardingIntroThenOldContextualOnboardingIsPresented() throws { + func testWhenPresentContextualOnboardingAndVariantDoesNotSupportContextualDaxDialogsThenOldContextualOnboardingIsPresented() throws { // GIVEN var variantManagerMock = MockVariantManager() variantManagerMock.isSupportedBlock = { feature in - feature != .newOnboardingIntro + feature != .contextualDaxDialogs } let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) let parent = TabViewControllerMock() @@ -57,11 +57,11 @@ final class ContextualOnboardingPresenterTests: XCTestCase { XCTAssertEqual(sender, DaxDialogs.BrowsingSpec.afterSearch) } - func testWhenPresentContextualOnboardingAndVariantSupportsNewOnboardingIntroThenThenNewContextualOnboardingIsPresented() { + func testWhenPresentContextualOnboardingAndVariantSupportsContextualDaxDialogsThenThenNewContextualOnboardingIsPresented() { // GIVEN var variantManagerMock = MockVariantManager() variantManagerMock.isSupportedBlock = { feature in - feature == .newOnboardingIntro + feature == .contextualDaxDialogs } let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) let parent = TabViewControllerMock() @@ -76,11 +76,34 @@ final class ContextualOnboardingPresenterTests: XCTestCase { XCTAssertNotNil(parent.capturedChild) } + func testWhenPresentContextualOnboardingAndVariantShouldNotShowContextualDialogsThenDoNothing() { + // GIVEN + var variantManagerMock = MockVariantManager() + variantManagerMock.currentVariant = MockVariant(features: [.newOnboardingIntro]) + let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) + let parent = TabViewControllerMock() + XCTAssertFalse(parent.didCallAddChild) + XCTAssertNil(parent.capturedChild) + XCTAssertFalse(parent.didCallPerformSegue) + XCTAssertNil(parent.capturedSegueIdentifier) + XCTAssertNil(parent.capturedSender) + + // WHEN + sut.presentContextualOnboarding(for: .afterSearch, in: parent) + + // THEN + XCTAssertFalse(parent.didCallAddChild) + XCTAssertNil(parent.capturedChild) + XCTAssertFalse(parent.didCallPerformSegue) + XCTAssertNil(parent.capturedSegueIdentifier) + XCTAssertNil(parent.capturedSender) + } + func testWhenPresentContextualOnboardingForFireEducational_andBarAtTheTop_TheMessageHandPointsInTheRightDirection() throws { // GIVEN var variantManagerMock = MockVariantManager() variantManagerMock.isSupportedBlock = { feature in - feature == .newOnboardingIntro + feature == .contextualDaxDialogs } let appSettings = AppSettingsMock() let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory, appSettings: appSettings) @@ -98,7 +121,7 @@ final class ContextualOnboardingPresenterTests: XCTestCase { // GIVEN var variantManagerMock = MockVariantManager() variantManagerMock.isSupportedBlock = { feature in - feature == .newOnboardingIntro + feature == .contextualDaxDialogs } let appSettings = AppSettingsMock() appSettings.currentAddressBarPosition = .bottom @@ -113,12 +136,12 @@ final class ContextualOnboardingPresenterTests: XCTestCase { XCTAssertTrue(view.message.string.contains("👇")) } - func testWhenDismissContextualOnboardingAndVariantSupportsNewOnboardingIntroThenContextualOnboardingIsDismissed() { + func testWhenDismissContextualOnboardingAndVariantSupportsContextualDaxDialogsThenContextualOnboardingIsDismissed() { // GIVEN let expectation = self.expectation(description: #function) var variantManagerMock = MockVariantManager() variantManagerMock.isSupportedBlock = { feature in - feature == .newOnboardingIntro + feature == .contextualDaxDialogs } let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) let parent = TabViewControllerMock() @@ -140,13 +163,13 @@ final class ContextualOnboardingPresenterTests: XCTestCase { XCTAssertFalse(parent.daxDialogsStackView.arrangedSubviews.contains(daxController.view)) } - func testWhenDismissContextualOnboardingAndVariantDoesNotSupportsNewOnboardingIntroThenNothingHappens() { + func testWhenDismissContextualOnboardingAndVariantDoesNotSupportsContextualDaxDialogsThenNothingHappens() { // GIVEN let expectation = self.expectation(description: #function) expectation.isInverted = true var variantManagerMock = MockVariantManager() variantManagerMock.isSupportedBlock = { feature in - feature != .newOnboardingIntro + feature != .contextualDaxDialogs } let sut = ContextualOnboardingPresenter(variantManager: variantManagerMock, daxDialogsFactory: contextualDaxDialogsFactory) let parent = TabViewControllerMock() diff --git a/DuckDuckGoTests/DaxDialogTests.swift b/DuckDuckGoTests/DaxDialogTests.swift index 7bf98a7423..2c637ba23b 100644 --- a/DuckDuckGoTests/DaxDialogTests.swift +++ b/DuckDuckGoTests/DaxDialogTests.swift @@ -904,6 +904,23 @@ final class DaxDialog: XCTestCase { XCTAssertEqual(result, .final) } + func testWhenExperimentGroup_AndNextHomeScreenMessageNewIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { + // GIVEN + let settings = InMemoryDaxDialogsSettings() + settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue + settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" + let sut = makeExperimentSUT(settings: settings) + XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) + XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) + + // WHEN + _ = sut.nextHomeScreenMessageNew() + + // THEN + XCTAssertNil(settings.lastShownContextualOnboardingDialogType) + XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) + } + func testWhenExperimentGroup_AndCanEnableAddFavoritesFlowIsCalled_ThenReturnFalse() { // GIVEN let sut = makeExperimentSUT(settings: InMemoryDaxDialogsSettings()) @@ -1019,6 +1036,40 @@ final class DaxDialog: XCTestCase { XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) } + func testWhenExperimentGroup_AndSetDaxDialogDismiss_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() { + // GIVEN + let settings = InMemoryDaxDialogsSettings() + settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue + settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" + let sut = makeExperimentSUT(settings: settings) + XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) + XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) + + // WHEN + sut.setDaxDialogDismiss() + + // THEN + XCTAssertNil(settings.lastShownContextualOnboardingDialogType) + XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) + } + + func testWhenExperimentGroup_AndClearedBrowserDataIsCalled_ThenLastVisitedOnboardingWebsiteAndLastShownDaxDialogAreSetToNil() throws { + // GIVEN + let settings = InMemoryDaxDialogsSettings() + settings.lastShownContextualOnboardingDialogType = DaxDialogs.BrowsingSpec.fire.type.rawValue + settings.lastVisitedOnboardingWebsiteURLPath = "https://www.example.com" + let sut = makeExperimentSUT(settings: settings) + XCTAssertNotNil(settings.lastShownContextualOnboardingDialogType) + XCTAssertNotNil(settings.lastVisitedOnboardingWebsiteURLPath) + + // WHEN + sut.clearedBrowserData() + + // THEN + XCTAssertNil(settings.lastShownContextualOnboardingDialogType) + XCTAssertNil(settings.lastVisitedOnboardingWebsiteURLPath) + } + func testWhenExperimentGroup_AndIsEnabledIsFalse_AndReloadWebsite_ThenReturnNilBrowsingSpec() throws { // GIVEN let lastVisitedWebsitePath = "https://www.example.com" @@ -1036,6 +1087,32 @@ final class DaxDialog: XCTestCase { XCTAssertNil(result) } + func testWhenIsEnabledIsCalled_AndShouldShowDaxDialogsIsTrue_ThenReturnTrue() { + // GIVEN + var mockVariantManager = MockVariantManager() + mockVariantManager.currentVariant = MockVariant(features: [.newOnboardingIntro, .contextualDaxDialogs]) + let sut = DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) + + // WHEN + let result = sut.isEnabled + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsEnabledIsCalled_AndShouldShowDaxDialogsIsFalse_ThenReturnFalse() { + // GIVEN + var mockVariantManager = MockVariantManager() + mockVariantManager.currentVariant = MockVariant(features: [.newOnboardingIntro]) + let sut = DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) + + // WHEN + let result = sut.isEnabled + + // THEN + XCTAssertFalse(result) + } + private func detectedTrackerFrom(_ url: URL, pageUrl: String) -> DetectedRequest { let entity = entityProvider.entity(forHost: url.host!) return DetectedRequest(url: url.absoluteString, @@ -1060,7 +1137,10 @@ final class DaxDialog: XCTestCase { } private func makeExperimentSUT(settings: DaxDialogsSettings) -> DaxDialogs { - let mockVariantManager = MockVariantManager(isSupportedReturns: true) + var mockVariantManager = MockVariantManager() + mockVariantManager.isSupportedBlock = { feature in + feature == .contextualDaxDialogs + } return DaxDialogs(settings: settings, entityProviding: entityProvider, variantManager: mockVariantManager) } } diff --git a/DuckDuckGoTests/DaxDialogsNewTabTests.swift b/DuckDuckGoTests/DaxDialogsNewTabTests.swift index 6561d1bed0..23f3617765 100644 --- a/DuckDuckGoTests/DaxDialogsNewTabTests.swift +++ b/DuckDuckGoTests/DaxDialogsNewTabTests.swift @@ -162,6 +162,18 @@ final class DaxDialogsNewTabTests: XCTestCase { XCTAssertNil(homeScreenMessage) } + func testIfFinalDialogShown_andBrowsingAfterSearchNotShown_OnNextHomeScreenMessageNew_ReturnsNil() { + // GIVEN + settings.browsingAfterSearchShown = false + settings.browsingFinalDialogShown = true + + // WHEN + let homeScreenMessage = daxDialogs.nextHomeScreenMessageNew() + + // THEN + XCTAssertNil(homeScreenMessage) + } + } class MockDaxDialogsSettings: DaxDialogsSettings { diff --git a/DuckDuckGoTests/DebouncerTests.swift b/DuckDuckGoTests/DebouncerTests.swift new file mode 100644 index 0000000000..eb1c2f21f1 --- /dev/null +++ b/DuckDuckGoTests/DebouncerTests.swift @@ -0,0 +1,81 @@ +// +// DebouncerTests.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 DebouncerTests: XCTestCase { + private var sut: Debouncer! + + override func setUpWithError() throws { + try super.setUpWithError() + sut = Debouncer() + } + + override func tearDownWithError() throws { + sut = nil + try super.tearDownWithError() + } + + func testWhenDebounceThenTriggerBlockAfterDueTime() { + // GIVEN + let expectation = expectation(description: #function) + + // WHEN + sut.debounce(for: 0.05) { + // THEN + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0.1) + } + + func testWhenCancelThenCancelBlockExecution() { + // GIVEN + let expectation = expectation(description: #function) + expectation.isInverted = true + sut.debounce(for: 0.03) { + // THEN + expectation.fulfill() + } + + // WHEN + sut.cancel() + + wait(for: [expectation], timeout: 0.1) + } + + func testWhenDebounceTwoBlocksThenCancelFirstTaskWhenSecondBlockIsScheduled() { + // GIVEN + let firstTaskExpectation = expectation(description: "FirstTask Completion") + firstTaskExpectation.isInverted = true + + let secondTaskExpectation = expectation(description: "Second Task Completion") + + // WHEN + sut.debounce(for: 0.05) { + firstTaskExpectation.fulfill() + } + sut.debounce(for: 0.02) { + secondTaskExpectation.fulfill() + } + + wait(for: [firstTaskExpectation, secondTaskExpectation], timeout: 0.1) + } +} diff --git a/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift b/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift new file mode 100644 index 0000000000..b9e460038d --- /dev/null +++ b/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift @@ -0,0 +1,198 @@ +// +// DefaultVariantManagerOnboardingTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import BrowserServicesKit +@testable import Core +@testable import DuckDuckGo + +final class DefaultVariantManagerOnboardingTests: XCTestCase { + + // MARK: - Is New Intro Flow + + func testWhenIsNewIntroFlow_AndFeatureIsNewOnboardingIntro_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntro]) + + // WHEN + let result = sut.isNewIntroFlow + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsNewIntroFlow_AndFeaturesContainNewOnboardingIntroHighlights_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntroHighlights]) + + // WHEN + let result = sut.isNewIntroFlow + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsNewIntroFlow_AndFeaturesDoNotContainNewOnboardingIntroOrNewOnboardingIntroHighlights_ThenReturnFalse() { + // GIVEN + let sut = makeVariantManager(features: [.contextualDaxDialogs]) + + // WHEN + let result = sut.isNewIntroFlow + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Is Onboarding Highlights + + func testWhenIsOnboardingHighlights_AndFeaturesContainOnboardingHighlights_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntroHighlights]) + + // WHEN + let result = sut.isOnboardingHighlightsExperiment + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsOnboardingHighlights_AndFeaturesDoNotContainOnboardingHighlights_ThenReturnFalse() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntro, .contextualDaxDialogs]) + + // WHEN + let result = sut.isOnboardingHighlightsExperiment + + // THEN + XCTAssertFalse(result) + } + + func testWhenIsOnboardingHighlights_AndFeaturesIsEmpty_ThenReturnFalse() { + // GIVEN + let sut = makeVariantManager(features: []) + + // WHEN + let result = sut.isOnboardingHighlightsExperiment + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Should Show Dax Dialogs + + func testWhenShouldShowDaxDialogs_AndFeaturesContainOnboardingIntro_ThenReturnFalse() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntro]) + + // WHEN + let result = sut.shouldShowDaxDialogs + + // THEN + XCTAssertFalse(result) + } + + func testWhenShouldShowDaxDialogs_AndFeaturesContainOnboardingIntroAndContextualDaxDialogs_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntro, .contextualDaxDialogs]) + + // WHEN + let result = sut.shouldShowDaxDialogs + + // THEN + XCTAssertTrue(result) + } + + func testWhenShouldShowDaxDialogs_AndFeaturesContainOnboardingHighlights_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntroHighlights]) + + // WHEN + let result = sut.shouldShowDaxDialogs + + // THEN + XCTAssertTrue(result) + } + + func testWhenShouldShowDaxDialogs_AndFeaturesIsEmpty_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: []) + + // WHEN + let result = sut.shouldShowDaxDialogs + + // THEN + XCTAssertTrue(result) + } + + // MARK: - Is Contextual Dax Dialogs Enabled + + func testWhenIsContextualDaxDialogsEnabled_AndFeaturesContainContextualDaxDialogs_ThenReturnTrue() { + // GIVEN + let sut = makeVariantManager(features: [.contextualDaxDialogs]) + + // WHEN + let result = sut.isContextualDaxDialogsEnabled + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsContextualDaxDialogsEnabled_AndFeaturesDoNotContainContextualDaxDialogs_ThenReturnFalse() { + // GIVEN + let sut = makeVariantManager(features: [.newOnboardingIntro, .newOnboardingIntroHighlights]) + + // WHEN + let result = sut.isContextualDaxDialogsEnabled + + // THEN + XCTAssertFalse(result) + } + + func testWhenIsContextualDaxDialogsEnabled_AndFeaturesIsEmpty_ThenReturnFalse() { + // GIVEN + let sut = makeVariantManager(features: []) + + // WHEN + let result = sut.isContextualDaxDialogsEnabled + + // THEN + XCTAssertFalse(result) + } + +} + +// MARK: Helpers + +private extension DefaultVariantManagerOnboardingTests { + + func makeVariantManager(features: [FeatureName]) -> DefaultVariantManager { + let mockStatisticStore = MockStatisticsStore() + mockStatisticStore.variant = #function + let variantManager = DefaultVariantManager( + variants: [VariantIOS(name: #function, weight: 1, isIncluded: VariantIOS.When.always, features: features)], + storage: mockStatisticStore, + rng: MockVariantRNG(returnValue: 500), + returningUserMeasurement: MockReturningUserMeasurement(), + variantNameOverride: MockVariantNameOverride() + ) + variantManager.assignVariantIfNeeded { _ in } + return variantManager + } + +} diff --git a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift b/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift index 1f1db8b0ca..e9d3ebf36b 100644 --- a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift @@ -92,9 +92,9 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { hvc = nil } - func testWhenNewOnboarding_OnDidAppear_CorrectTypePassedToDialogFactory() throws { + func testWhenContextualDaxDialogsSupported_OnDidAppear_CorrectTypePassedToDialogFactory() throws { // GIVEN - variantManager.isSupported = true + variantManager.supportedFeatures = [.contextualDaxDialogs] let expectedSpec = randomDialogType() specProvider.specToReturn = expectedSpec @@ -102,16 +102,33 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { hvc.viewDidAppear(false) // THEN - XCTAssertEqual(self.variantManager.capturedFeatureName?.rawValue, FeatureName.newOnboardingIntro.rawValue) + XCTAssertEqual(self.variantManager.capturedFeatureName?.rawValue, FeatureName.contextualDaxDialogs.rawValue) XCTAssertFalse(self.specProvider.nextHomeScreenMessageCalled) XCTAssertTrue(self.specProvider.nextHomeScreenMessageNewCalled) XCTAssertEqual(self.dialogFactory.homeDialog, expectedSpec) XCTAssertNotNil(self.dialogFactory.onDismiss) } + func testWhenDaxDialogsAreNotEnabled_OnDidAppear_NothingHappens() throws { + // GIVEN + variantManager.currentVariant = MockVariant(features: [.newOnboardingIntro]) + let expectedSpec = randomDialogType() + specProvider.specToReturn = expectedSpec + + // WHEN + hvc.viewDidAppear(false) + + // THEN + XCTAssertNil(self.variantManager.capturedFeatureName) + XCTAssertFalse(self.specProvider.nextHomeScreenMessageCalled) + XCTAssertFalse(self.specProvider.nextHomeScreenMessageNewCalled) + XCTAssertNil(self.dialogFactory.homeDialog) + XCTAssertNil(self.dialogFactory.onDismiss) + } + func testWhenOldOnboarding_OnDidAppear_NothingPassedDialogFactory() throws { // GIVEN - variantManager.isSupported = false + variantManager.supportedFeatures = [] // WHEN hvc.viewDidAppear(false) @@ -123,9 +140,9 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { XCTAssertNil(dialogFactory.onDismiss) } - func testWhenNewOnboarding_OnOnboardingComplete_CorrectTypePassedToDialogFactory() throws { + func testWhenContextualDaxDialogsSupported_OnOnboardingComplete_CorrectTypePassedToDialogFactory() throws { // GIVEN - variantManager.isSupported = true + variantManager.supportedFeatures = [.contextualDaxDialogs] let expectedSpec = randomDialogType() specProvider.specToReturn = expectedSpec @@ -133,7 +150,7 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { hvc.onboardingCompleted() // THEN - XCTAssertEqual(self.variantManager.capturedFeatureName?.rawValue, FeatureName.newOnboardingIntro.rawValue) + XCTAssertEqual(self.variantManager.capturedFeatureName?.rawValue, FeatureName.contextualDaxDialogs.rawValue) XCTAssertFalse(self.specProvider.nextHomeScreenMessageCalled) XCTAssertTrue(self.specProvider.nextHomeScreenMessageNewCalled) XCTAssertEqual(self.dialogFactory.homeDialog, expectedSpec) @@ -142,7 +159,7 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { func testWhenOldOnboarding_OnOnboardingComplete_NothingPassedDialogFactory() throws { // GIVEN - variantManager.isSupported = false + variantManager.supportedFeatures = [] // WHEN hvc.onboardingCompleted() @@ -156,7 +173,7 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { func testWhenOldOnboarding_OnOpenedAsNewTab_NothingPassedDialogFactory() throws { // GIVEN - variantManager.isSupported = false + variantManager.supportedFeatures = [] // WHEN hvc.openedAsNewTab(allowingKeyboard: true) @@ -168,6 +185,30 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { XCTAssertNil(dialogFactory.onDismiss) } + func testWhenShowNextDaxDialog_AndShouldShowDaxDialogs_ThenReturnTrue() { + // GIVEN + variantManager.supportedFeatures = [] + + // WHEN + hvc.showNextDaxDialog() + + // THEN + XCTAssertTrue(specProvider.nextHomeScreenMessageCalled) + XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) + } + + func testWhenShowNextDaxDialog_AndShouldNotShowDaxDialogs_ThenReturnFalse() { + // GIVEN + variantManager.currentVariant = MockVariant(features: [.newOnboardingIntro]) + + // WHEN + hvc.showNextDaxDialog() + + // THEN + XCTAssertFalse(specProvider.nextHomeScreenMessageCalled) + XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) + } + private func randomDialogType() -> DaxDialogs.HomeScreenSpec { let specs: [DaxDialogs.HomeScreenSpec] = [.initial, .subsequent, .final, .addFavorite] return specs.randomElement()! @@ -177,14 +218,14 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { class CapturingVariantManager: VariantManager { var currentVariant: Variant? var capturedFeatureName: FeatureName? - var isSupported = false + var supportedFeatures: [FeatureName] = [] func assignVariantIfNeeded(_ newInstallCompletion: (BrowserServicesKit.VariantManager) -> Void) { } func isSupported(feature: FeatureName) -> Bool { capturedFeatureName = feature - return isSupported + return supportedFeatures.contains(feature) } } @@ -233,3 +274,14 @@ class MockNewTabDialogSpecProvider: NewTabDialogSpecProvider { dismissCalled = true } } + +struct MockVariant: Variant { + var name: String = "" + var weight: Int = 0 + var isIncluded: () -> Bool = { false } + var features: [BrowserServicesKit.FeatureName] = [] + + init(features: [BrowserServicesKit.FeatureName]) { + self.features = features + } +} diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index 4b9fa4919c..3cd9a7c74c 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -76,8 +76,6 @@ final class MockTabDelegate: TabDelegate { func tabContentProcessDidTerminate(tab: DuckDuckGo.TabViewController) {} - func tabDidRequestForgetAll(tab: DuckDuckGo.TabViewController) {} - func tabDidRequestFireButtonPulse(tab: DuckDuckGo.TabViewController) { didRequestFireButtonPulseCalled = true } diff --git a/DuckDuckGoTests/OnboardingManagerTests.swift b/DuckDuckGoTests/OnboardingManagerTests.swift index ee456fb41d..b0f90803d5 100644 --- a/DuckDuckGoTests/OnboardingManagerTests.swift +++ b/DuckDuckGoTests/OnboardingManagerTests.swift @@ -25,17 +25,20 @@ final class OnboardingManagerTests: XCTestCase { private var sut: OnboardingManager! private var appSettingsMock: AppSettingsMock! private var featureFlaggerMock: MockFeatureFlagger! + private var variantManagerMock: MockVariantManager! override func setUpWithError() throws { try super.setUpWithError() appSettingsMock = AppSettingsMock() featureFlaggerMock = MockFeatureFlagger() - sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock) + variantManagerMock = MockVariantManager() + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock) } override func tearDownWithError() throws { appSettingsMock = nil featureFlaggerMock = nil + variantManagerMock = nil sut = nil try super.tearDownWithError() } @@ -119,4 +122,32 @@ final class OnboardingManagerTests: XCTestCase { // THEN XCTAssertTrue(result) } + + func testWhenIsOnboardingHiglightsEnabledAndVariantManagerSupportOnboardingHighlightsReturnTrue() { + // GIVEN + variantManagerMock.isSupportedBlock = { _ in true } + appSettingsMock.onboardingHighlightsEnabled = false + featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock) + + // WHEN + let result = sut.isOnboardingHighlightsEnabled + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsOnboardingHiglightsEnabledAndVariantManagerSupportOnboardingHighlightsReturnFalse() { + // GIVEN + variantManagerMock.isSupportedBlock = { _ in false } + appSettingsMock.onboardingHighlightsEnabled = false + featureFlaggerMock.enabledFeatureFlags = [FeatureFlag.onboardingHighlights] + sut = OnboardingManager(appDefaults: appSettingsMock, featureFlagger: featureFlaggerMock, variantManager: variantManagerMock) + + // WHEN + let result = sut.isOnboardingHighlightsEnabled + + // THEN + XCTAssertFalse(result) + } } diff --git a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift index 59456d6edc..ced3c4b449 100644 --- a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift +++ b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift @@ -25,6 +25,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { private var onboardingManagerMock: OnboardingManagerMock! let userText = UserText.DaxOnboardingExperiment.ContextualOnboarding.self let highlightsUserText = UserText.HighlightsOnboardingExperiment.ContextualOnboarding.self + static let imageSearch = "!image " override func setUpWithError() throws { try super.setUpWithError() @@ -37,7 +38,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { } func testSearchesListForEnglishLanguageAndUsRegion() { - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "en") let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ @@ -51,7 +52,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { } func testSearchesListForNonEnglishLanguageAndNonUSRegion() { - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "FR", languageCode: "fr") let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ @@ -65,7 +66,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { } func testSearchesListForUSRegionAndNonEnglishLanguage() { - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "es") let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ @@ -82,13 +83,13 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testWhenHighlightsOnboardingAndSearchesListForEnglishLanguageAndUsRegionThenDoNotReturnOption3() { onboardingManagerMock.isOnboardingHighlightsEnabled = true - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "en") let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), - ContextualOnboardingListItem.surprise(title: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ContextualOnboardingListItem.surprise(title: Self.imageSearch + highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") ] XCTAssertEqual(provider.list, expectedSearches) @@ -96,13 +97,13 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testWhenHighlightsOnboardingAndSearchesListForNonEnglishLanguageAndNonUSRegionThenDoNotReturnOption3() { onboardingManagerMock.isOnboardingHighlightsEnabled = true - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "FR", languageCode: "fr") let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), ContextualOnboardingListItem.search(title: userText.tryASearchOption2International), - ContextualOnboardingListItem.surprise(title: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ContextualOnboardingListItem.surprise(title: Self.imageSearch + highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") ] XCTAssertEqual(provider.list, expectedSearches) @@ -110,13 +111,13 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testWhenHighlightsOnboardingAndSearchesListForUSRegionAndNonEnglishLanguageThenDoNotReturnOption3() { onboardingManagerMock.isOnboardingHighlightsEnabled = true - let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "US", languageCode: "es") let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), - ContextualOnboardingListItem.surprise(title: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ContextualOnboardingListItem.surprise(title: Self.imageSearch + highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") ] XCTAssertEqual(provider.list, expectedSearches) diff --git a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift index ac1a8d22df..5bafc76247 100644 --- a/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/TabViewControllerDaxDialogTests.swift @@ -121,6 +121,17 @@ final class TabViewControllerDaxDialogTests: XCTestCase { XCTAssertTrue(onboardingPresenterMock.didCallDismissContextualOnboardingIfNeeded) } + func testWhenDidTapDismissActionIsCalledThenAskDaxDialogsLogicToSetDialogDismiss() { + // GIVEN + XCTAssertFalse(onboardingLogicMock.didCallSetDaxDialogDismiss) + + // WHEN + sut.didTapDismissContextualOnboardingAction() + + // THEN + XCTAssertTrue(onboardingLogicMock.didCallSetDaxDialogDismiss) + } + func testWhenDidAcknowledgedTrackersDialogIsCalledThenSetFireEducationMessageSeenIsCalledOnLogic() { // GIVEN XCTAssertFalse(onboardingLogicMock.didCallSetFireEducationMessageSeen) @@ -222,6 +233,8 @@ final class ContextualOnboardingLogicMock: ContextualOnboardingLogic { private(set) var didCallsetsetSearchMessageSeen = false private(set) var didCallCanEnableAddFavoriteFlow = false private(set) var didCallEnableAddFavoriteFlow = false + private(set) var didCallSetDaxDialogDismiss = false + private(set) var didCallClearedBrowserData = false var canStartFavoriteFlow = false @@ -255,7 +268,15 @@ final class ContextualOnboardingLogicMock: ContextualOnboardingLogic { func enableAddFavoriteFlow() { didCallEnableAddFavoriteFlow = true } - + + func setDaxDialogDismiss() { + didCallSetDaxDialogDismiss = true + } + + func clearedBrowserData() { + didCallClearedBrowserData = true + } + } extension WKNavigation { From 059bc1a98e3c4b42700a6de21e0da5e713fc8531 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 26 Sep 2024 21:39:15 +1000 Subject: [PATCH 41/46] Release 7.139.0-3 (#3399) --- 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 f9a6a04a9e..24057da877 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9170,7 +9170,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9207,7 +9207,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9297,7 +9297,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9324,7 +9324,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9473,7 +9473,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9498,7 +9498,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9567,7 +9567,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9601,7 +9601,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9634,7 +9634,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9664,7 +9664,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9974,7 +9974,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10005,7 +10005,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10033,7 +10033,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10066,7 +10066,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10096,7 +10096,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10129,11 +10129,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10366,7 +10366,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10393,7 +10393,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10425,7 +10425,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10462,7 +10462,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10497,7 +10497,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10532,11 +10532,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10709,11 +10709,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10742,10 +10742,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From e17d889b306f087c1b745bbfd8550bdf3f223c2b Mon Sep 17 00:00:00 2001 From: David Harbage Date: Thu, 26 Sep 2024 11:45:58 -0400 Subject: [PATCH 42/46] Bump BSK to pull in C-S-S 6.19.0 (#3396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1207836782368194/f Tech Design URL: https://app.asana.com/0/72649045549333/1207836782368196/f Corresponding BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/1006 Corresponding macOS PR: https://github.com/duckduckgo/macos-browser/pull/3347 CC: @jonathanKingston **Description**: This PR bumps the BSK pin to pull in the changes from https://github.com/duckduckgo/content-scope-scripts/pull/1026. **Steps to test this PR**: 1. Navigate to any old [MSN article page](https://www.msn.com/en-us/sports/nfl/how-saquon-barkley-gave-eagles-coach-a-hard-lesson-in-accountability-while-saving-season/ar-AA1qPl89?ocid=hpmsn&cvid=81d6bc9eb97e442ba7de62c248189b23&ei=12) and refresh the page two or more times. See that the Expand Article button persists across reloads. **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) --- 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 fdfe390f62..10ac3c7192 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10935,7 +10935,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.1.0; + version = 198.1.1; }; }; 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 d8b3ac6978..0df8fdf10d 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" : "4db50292abf1180d66da55cf83f75d37395df1f9", - "version" : "198.1.0" + "revision" : "5b59c2790a7f7c69bf1f6793152bdb4ea344b1b4", + "version" : "198.1.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1", - "version" : "6.17.0" + "revision" : "1ed569676555d493c9c5575eaed22aa02569aac9", + "version" : "6.19.0" } }, { From ccdb203b7ca2799f78810bcb25a412aa6fcc5b0d Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 27 Sep 2024 16:51:27 +0200 Subject: [PATCH 43/46] For third party requests differentiate if they are affiliated with first party (#3386) Task/Issue URL: https://app.asana.com/0/414709148257752/1208376794617030/f **Description**: Fixed in BSK. When loading a website and some of the allowed third party requests may not be recognized as trackers but by their URL they may belong to the same entity as the website. For that cases we should mark their state as `.allowed(reason: .ownedByFirstParty)` (instead of `.allowed(reason: .otherThirdPartyRequest)`). --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 10ac3c7192..ecc72d710f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10935,7 +10935,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.1.1; + version = 198.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 0df8fdf10d..61f102813b 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" : "5b59c2790a7f7c69bf1f6793152bdb4ea344b1b4", - "version" : "198.1.1" + "revision" : "20469bbeeff33fcd18e78f672a544ee82b4a741c", + "version" : "198.2.0" } }, { From 904633668ff898dc21cf41a631b9cc1e84d37f6c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 27 Sep 2024 16:42:54 +0100 Subject: [PATCH 44/46] fix suggestions performance (#3405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201048563534612/1208413716679959/f Tech Design URL: CC: **Description**: Bump BSK to include suggestions performance fix. **Steps to test this PR**: 1. see https://github.com/duckduckgo/BrowserServicesKit/pull/1008 * [ ] 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) --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +++--- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ecc72d710f..689c61c817 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -692,9 +692,9 @@ 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; - 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */; }; 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */; }; + 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; @@ -2501,9 +2501,9 @@ 98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = ""; }; 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = ""; }; - 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultVariantManager+Onboarding.swift"; sourceTree = ""; }; 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVariantManagerOnboardingTests.swift; sourceTree = ""; }; + 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; @@ -10935,7 +10935,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.2.0; + version = 198.2.1; }; }; 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 61f102813b..9f70b65f0c 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" : "20469bbeeff33fcd18e78f672a544ee82b4a741c", - "version" : "198.2.0" + "revision" : "b60b38bace7262e0c4a006018b7e4b060ba4b754", + "version" : "198.2.1" } }, { From 4a6642a5730d50e3fe8e1be2fa238e05aa9107ee Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Sat, 28 Sep 2024 02:14:22 +1000 Subject: [PATCH 45/46] Onboarding highlights experiment updates (#3406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1206329551987282/1208416729129331/f **Description**: 1. Updates the experiment setup based on [the latest experiment analysis conversation](https://app.asana.com/0/0/1208314441219794/1208411648765130/f) 2. Add atb for old dax dialogs. This was already agreed on Privacy Triage but was missed in previous implementation. **Steps to test this PR**: 1. Ensure unit tests pass. 1. Test that “ms” variant shows new intro and old Dax Dialogs. 2. Test that “mu” variant shows new onboarding intro and new in context dax dialogs. 3. Test that “mx” variant shows onboarding highlights and new in context dax dialogs. **Definition of Done (Internal Only)**: * [x] 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) --- DuckDuckGo/DaxDialogs.swift | 1 - .../FullscreenDaxDialogViewController.swift | 2 +- DuckDuckGo/HomeViewController.swift | 2 - DuckDuckGo/MainViewController.swift | 3 +- DuckDuckGo/NewTabPageViewController.swift | 4 +- .../ContextualOnboardingPresenter.swift | 3 -- .../DefaultVariantManager+Onboarding.swift | 6 --- DuckDuckGo/TabSwitcherViewController.swift | 8 +--- DuckDuckGo/TabsBarViewController.swift | 9 +--- .../ContextualOnboardingPresenterTests.swift | 23 ---------- DuckDuckGoTests/DaxDialogTests.swift | 13 ------ ...DefaultVariantManagerOnboardingTests.swift | 46 ------------------- .../HomeViewControllerDaxDialogTests.swift | 29 ------------ 13 files changed, 5 insertions(+), 144 deletions(-) diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index 8fc63f8e3a..d1793efb6b 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -279,7 +279,6 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic { var isEnabled: Bool { // skip dax dialogs in integration tests guard ProcessInfo.processInfo.environment["DAXDIALOGS"] != "false" else { return false } - guard variantManager.shouldShowDaxDialogs else { return false } return !settings.isDismissed } diff --git a/DuckDuckGo/FullscreenDaxDialogViewController.swift b/DuckDuckGo/FullscreenDaxDialogViewController.swift index 3a49583756..e9bf422811 100644 --- a/DuckDuckGo/FullscreenDaxDialogViewController.swift +++ b/DuckDuckGo/FullscreenDaxDialogViewController.swift @@ -79,7 +79,7 @@ class FullscreenDaxDialogViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let spec = spec { - Pixel.fire(pixel: spec.pixelName, withAdditionalParameters: [ "wo": woShown ? "1" : "0" ]) + Pixel.fire(pixel: spec.pixelName, withAdditionalParameters: [ "wo": woShown ? "1" : "0" ], includedParameters: [.appVersion, .atb]) } containerHeight.constant = daxDialogViewController?.calculateHeight() ?? 0 daxDialogViewController?.start() diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index 71bb361a88..0ff2977bb3 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -258,8 +258,6 @@ class HomeViewController: UIViewController, NewTabPage { } func presentNextDaxDialog() { - guard variantManager.shouldShowDaxDialogs else { return } - if variantManager.isContextualDaxDialogsEnabled { showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) } else { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 5b1f33e12f..6233baf103 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -2737,8 +2737,7 @@ extension MainViewController: AutoClearWorker { self.privacyProDataReporter.saveFireCount() // Ideally this should happen once data clearing has finished AND the animation is finished - // `showNextDaxDialog: true` only set from old onboarding FireDialog ActionSheet - if showNextDaxDialog && self.variantManager.shouldShowDaxDialogs { + if showNextDaxDialog { self.homeController?.showNextDaxDialog() } else if KeyboardSettings().onNewTab { let showKeyboardAfterFireButton = DispatchWorkItem { diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index c8ae9157bb..ffd3e9af70 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -165,9 +165,7 @@ final class NewTabPageViewController: UIHostingController DetectedRequest { let entity = entityProvider.entity(forHost: url.host!) return DetectedRequest(url: url.absoluteString, diff --git a/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift b/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift index b9e460038d..23d5d06c5f 100644 --- a/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift +++ b/DuckDuckGoTests/DefaultVariantManagerOnboardingTests.swift @@ -94,52 +94,6 @@ final class DefaultVariantManagerOnboardingTests: XCTestCase { XCTAssertFalse(result) } - // MARK: - Should Show Dax Dialogs - - func testWhenShouldShowDaxDialogs_AndFeaturesContainOnboardingIntro_ThenReturnFalse() { - // GIVEN - let sut = makeVariantManager(features: [.newOnboardingIntro]) - - // WHEN - let result = sut.shouldShowDaxDialogs - - // THEN - XCTAssertFalse(result) - } - - func testWhenShouldShowDaxDialogs_AndFeaturesContainOnboardingIntroAndContextualDaxDialogs_ThenReturnTrue() { - // GIVEN - let sut = makeVariantManager(features: [.newOnboardingIntro, .contextualDaxDialogs]) - - // WHEN - let result = sut.shouldShowDaxDialogs - - // THEN - XCTAssertTrue(result) - } - - func testWhenShouldShowDaxDialogs_AndFeaturesContainOnboardingHighlights_ThenReturnTrue() { - // GIVEN - let sut = makeVariantManager(features: [.newOnboardingIntroHighlights]) - - // WHEN - let result = sut.shouldShowDaxDialogs - - // THEN - XCTAssertTrue(result) - } - - func testWhenShouldShowDaxDialogs_AndFeaturesIsEmpty_ThenReturnTrue() { - // GIVEN - let sut = makeVariantManager(features: []) - - // WHEN - let result = sut.shouldShowDaxDialogs - - // THEN - XCTAssertTrue(result) - } - // MARK: - Is Contextual Dax Dialogs Enabled func testWhenIsContextualDaxDialogsEnabled_AndFeaturesContainContextualDaxDialogs_ThenReturnTrue() { diff --git a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift b/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift index e9d3ebf36b..66c9506142 100644 --- a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift @@ -109,23 +109,6 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { XCTAssertNotNil(self.dialogFactory.onDismiss) } - func testWhenDaxDialogsAreNotEnabled_OnDidAppear_NothingHappens() throws { - // GIVEN - variantManager.currentVariant = MockVariant(features: [.newOnboardingIntro]) - let expectedSpec = randomDialogType() - specProvider.specToReturn = expectedSpec - - // WHEN - hvc.viewDidAppear(false) - - // THEN - XCTAssertNil(self.variantManager.capturedFeatureName) - XCTAssertFalse(self.specProvider.nextHomeScreenMessageCalled) - XCTAssertFalse(self.specProvider.nextHomeScreenMessageNewCalled) - XCTAssertNil(self.dialogFactory.homeDialog) - XCTAssertNil(self.dialogFactory.onDismiss) - } - func testWhenOldOnboarding_OnDidAppear_NothingPassedDialogFactory() throws { // GIVEN variantManager.supportedFeatures = [] @@ -197,18 +180,6 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) } - func testWhenShowNextDaxDialog_AndShouldNotShowDaxDialogs_ThenReturnFalse() { - // GIVEN - variantManager.currentVariant = MockVariant(features: [.newOnboardingIntro]) - - // WHEN - hvc.showNextDaxDialog() - - // THEN - XCTAssertFalse(specProvider.nextHomeScreenMessageCalled) - XCTAssertFalse(specProvider.nextHomeScreenMessageNewCalled) - } - private func randomDialogType() -> DaxDialogs.HomeScreenSpec { let specs: [DaxDialogs.HomeScreenSpec] = [.initial, .subsequent, .final, .addFavorite] return specs.randomElement()! From 974fca116904b86ce230aaae23721b8fe89fb09e Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:19:00 -0400 Subject: [PATCH 46/46] Release 7.139.0-4 (#3411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --------- Co-authored-by: Mariusz Śpiewak Co-authored-by: Christopher Brind Co-authored-by: Fernando Bunn Co-authored-by: Daniel Bernal Co-authored-by: Alessandro Boron Co-authored-by: David Harbage Co-authored-by: Michal Smaga --- .github/workflows/end-to-end.yml | 1 - .github/workflows/pr-task-url.yml | 6 +- .maestro/release_tests/tabs.yaml | 44 ++++ Core/AppURLs.swift | 2 +- Core/PixelEvent.swift | 2 - DuckDuckGo.xcodeproj/project.pbxproj | 106 ++++----- .../xcshareddata/swiftpm/Package.resolved | 8 +- ...emView.swift => FavoriteAddItemView.swift} | 6 +- DuckDuckGo/FavoriteItem.swift | 5 +- ...wift => FavoritePlaceholderItemView.swift} | 6 +- DuckDuckGo/FavoritesDefaultViewModel.swift | 218 ------------------ DuckDuckGo/FavoritesEmptyStateView.swift | 78 ------- DuckDuckGo/FavoritesPreviewDataSource.swift | 2 +- DuckDuckGo/FavoritesSectionHeader.swift | 49 ---- DuckDuckGo/FavoritesView.swift | 11 +- DuckDuckGo/FavoritesViewModel.swift | 207 +++++++++++++++-- DuckDuckGo/NewTabPageView.swift | 32 +-- DuckDuckGo/NewTabPageViewController.swift | 8 +- DuckDuckGo/UserText.swift | 2 - DuckDuckGo/bg.lproj/Localizable.strings | 3 - DuckDuckGo/cs.lproj/Localizable.strings | 3 - DuckDuckGo/da.lproj/Localizable.strings | 3 - DuckDuckGo/de.lproj/Localizable.strings | 3 - DuckDuckGo/el.lproj/Localizable.strings | 3 - DuckDuckGo/en.lproj/Localizable.strings | 3 - DuckDuckGo/es.lproj/Localizable.strings | 3 - DuckDuckGo/et.lproj/Localizable.strings | 3 - DuckDuckGo/fi.lproj/Localizable.strings | 3 - DuckDuckGo/fr.lproj/Localizable.strings | 3 - DuckDuckGo/hr.lproj/Localizable.strings | 3 - DuckDuckGo/hu.lproj/Localizable.strings | 3 - DuckDuckGo/it.lproj/Localizable.strings | 3 - DuckDuckGo/lt.lproj/Localizable.strings | 3 - DuckDuckGo/lv.lproj/Localizable.strings | 3 - DuckDuckGo/nb.lproj/Localizable.strings | 3 - DuckDuckGo/nl.lproj/Localizable.strings | 3 - DuckDuckGo/pl.lproj/Localizable.strings | 3 - DuckDuckGo/pt.lproj/Localizable.strings | 3 - DuckDuckGo/ro.lproj/Localizable.strings | 3 - DuckDuckGo/ru.lproj/Localizable.strings | 3 - DuckDuckGo/sk.lproj/Localizable.strings | 3 - DuckDuckGo/sl.lproj/Localizable.strings | 3 - DuckDuckGo/sv.lproj/Localizable.strings | 3 - DuckDuckGo/tr.lproj/Localizable.strings | 3 - .../NewTabPageFavoritesModelTests.swift | 55 ++++- 45 files changed, 362 insertions(+), 561 deletions(-) rename DuckDuckGo/{AddFavoritePlaceholderItemView.swift => FavoriteAddItemView.swift} (89%) rename DuckDuckGo/{FavoriteEmptyStateItem.swift => FavoritePlaceholderItemView.swift} (89%) delete mode 100644 DuckDuckGo/FavoritesDefaultViewModel.swift delete mode 100644 DuckDuckGo/FavoritesEmptyStateView.swift delete mode 100644 DuckDuckGo/FavoritesSectionHeader.swift diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index 3576b629c8..3e6b9b658b 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -103,7 +103,6 @@ jobs: steps: - name: Create Asana task when workflow failed - if: ${{ failure() }} run: | curl -s "https://app.asana.com/api/1.0/tasks" \ --header "Accept: application/json" \ diff --git a/.github/workflows/pr-task-url.yml b/.github/workflows/pr-task-url.yml index dad73e8220..7f130fe540 100644 --- a/.github/workflows/pr-task-url.yml +++ b/.github/workflows/pr-task-url.yml @@ -2,7 +2,7 @@ name: Asana PR Task URL on: pull_request: - types: [opened, edited, closed, synchronize, review_requested] + types: [opened, edited, closed, synchronize, review_requested, ready_for_review] jobs: @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} + outputs: task_id: ${{ steps.get-task-id.outputs.task_id }} task_in_project: ${{ steps.check-board-membership.outputs.task_in_project }} @@ -47,7 +49,7 @@ jobs: - name: Add Task to the App Board Project id: add-task-to-project - if: ${{ github.event.action == 'opened' && steps.check-board-membership.outputs.task_in_project == '0' }} + if: ${{ (github.event.action == 'opened' || github.event.action == 'ready_for_review') && steps.check-board-membership.outputs.task_in_project == '0' }} env: ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} ASANA_PROJECT_ID: ${{ vars.IOS_APP_BOARD_ASANA_PROJECT_ID }} diff --git a/.maestro/release_tests/tabs.yaml b/.maestro/release_tests/tabs.yaml index 40ae909d64..72618da2e2 100644 --- a/.maestro/release_tests/tabs.yaml +++ b/.maestro/release_tests/tabs.yaml @@ -46,6 +46,27 @@ tags: - assertVisible: ".*Privacy Test Pages.*" - tapOn: "Refresh Page" +# Suggestions +- assertVisible: + id: "searchEntry" + +- tapOn: + id: "searchEntry" +- inputText: "ad click" +- assertVisible: "Switch to Tab.*search-company.site" +- tapOn: "Switch to Tab.*search-company.site" +- assertVisible: ".*Ad Click Flow.*" + +- tapOn: + id: "searchEntry" +- inputText: "privacy" +- assertVisible: "Switch to Tab.*privacy-test-pages.site" +- tapOn: "Switch to Tab.*privacy-test-pages.site" +- assertVisible: ".*Privacy Test Pages.*" + +# Needed or else test can't see the Tab Switcher button for some reason +- tapOn: "Refresh Page" + # Close Tab - assertVisible: Tab Switcher - tapOn: Tab Switcher @@ -57,3 +78,26 @@ tags: - assertNotVisible: ".*Ad Click Flow.*" - assertVisible: "1 Private Tab" - tapOn: "Done" + +# Switch tabs from new tab +- tapOn: "Refresh Page" +- assertVisible: Tab Switcher +- tapOn: Tab Switcher +- assertVisible: ".*Privacy Test Pages.*" +- assertVisible: + id: "Add" +- tapOn: + id: "Add" +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "privacy" +- assertVisible: "Switch to Tab.*privacy-test-pages.site" +- tapOn: "Switch to Tab.*privacy-test-pages.site" +- assertVisible: ".*Privacy Test Pages.*" +- tapOn: "Refresh Page" +- assertVisible: Tab Switcher +- tapOn: Tab Switcher +- assertVisible: "1 Private Tab" + diff --git a/Core/AppURLs.swift b/Core/AppURLs.swift index e07d771f2a..56542c2066 100644 --- a/Core/AppURLs.swift +++ b/Core/AppURLs.swift @@ -35,7 +35,7 @@ public extension URL { static let emailProtectionSupportLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/email/settings/support"))! static let emailProtectionHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/email-protection/what-is-duckduckgo-email-protection/"))! static let aboutLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/about"))! - static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps"))! + static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps?origin=funnel_app_ios"))! static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))! static let autofillHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/sync-and-backup/password-manager-security/"))! diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index d7ff07905b..5f450c2359 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -766,7 +766,6 @@ extension Pixel { case newTabPageMessageDismissed case newTabPageFavoritesPlaceholderTapped - case newTabPageFavoritesInfoTooltip case newTabPageFavoritesSeeMore case newTabPageFavoritesSeeLess @@ -1581,7 +1580,6 @@ extension Pixel.Event { case .newTabPageMessageDismissed: return "m_new_tab_page_message_dismissed" case .newTabPageFavoritesPlaceholderTapped: return "m_new_tab_page_favorites_placeholder_click" - case .newTabPageFavoritesInfoTooltip: return "m_new_tab_page_favorites_info_tooltip" case .newTabPageFavoritesSeeMore: return "m_new_tab_page_favorites_see_more" case .newTabPageFavoritesSeeLess: return "m_new_tab_page_favorites_see_less" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 24057da877..af0edcc293 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -326,10 +326,9 @@ 6FABAA692C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */; }; 6FB1FE9E2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */; }; 6FB1FEA22C256ACD0075B68B /* NewTabPageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */; }; - 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */; }; - 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */; }; + 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */; }; 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */; }; - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */; }; + 6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */; }; 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; }; 6FD0C41F2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */; }; 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */; }; @@ -338,7 +337,6 @@ 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */; }; - 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */; }; 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */; }; 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */; }; 6FD8E51E2C5B84DE00345670 /* NewTabPageIntroMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */; }; @@ -347,8 +345,7 @@ 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */; }; 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */; }; - 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */; }; - 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; + 6FDC64052C98515E00DB71B3 /* FavoriteAddItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */; }; 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127372C20492500EB5724 /* NewTabPage.swift */; }; 6FE1273A2C204BD000EB5724 /* NewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127392C204BD000EB5724 /* NewTabPageView.swift */; }; @@ -695,9 +692,9 @@ 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; - 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F1061652C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */; }; 9F1623092C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */; }; + 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; @@ -1610,10 +1607,9 @@ 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsDebugView.swift; sourceTree = ""; }; 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageManager.swift; sourceTree = ""; }; - 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteEmptyStateItem.swift; sourceTree = ""; }; - 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesEmptyStateView.swift; sourceTree = ""; }; + 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritePlaceholderItemView.swift; sourceTree = ""; }; 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageGridView.swift; sourceTree = ""; }; - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDefaultViewModel.swift; sourceTree = ""; }; + 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetupTests.swift; sourceTree = ""; }; 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewModelTests.swift; sourceTree = ""; }; @@ -1622,7 +1618,6 @@ 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOrientationEnvironmentValue.swift; sourceTree = ""; }; - 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewDataSource.swift; sourceTree = ""; }; 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDelegate.swift; sourceTree = ""; }; 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageView.swift; sourceTree = ""; }; @@ -1631,8 +1626,7 @@ 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroDataStoring.swift; sourceTree = ""; }; 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStore.swift; sourceTree = ""; }; - 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFavoritePlaceholderItemView.swift; sourceTree = ""; }; - 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; + 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteAddItemView.swift; sourceTree = ""; }; 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FE127372C20492500EB5724 /* NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPage.swift; sourceTree = ""; }; 6FE127392C204BD000EB5724 /* NewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageView.swift; sourceTree = ""; }; @@ -2507,9 +2501,9 @@ 98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = ""; }; 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = ""; }; - 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F1061642C9C013F008DD5A0 /* DefaultVariantManager+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultVariantManager+Onboarding.swift"; sourceTree = ""; }; 9F1623082C9D14F10093C4FC /* DefaultVariantManagerOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVariantManagerOnboardingTests.swift; sourceTree = ""; }; + 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; @@ -3901,10 +3895,9 @@ 6FA3438D2C3D3BB800470677 /* Model */ = { isa = PBXGroup; children = ( - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */, + 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */, 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */, 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, - 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, @@ -3917,7 +3910,8 @@ children = ( 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */, 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */, - 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */, + 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */, + 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */, ); name = Item; sourceTree = ""; @@ -3930,16 +3924,6 @@ name = NewTabPageSectionsDebugView; sourceTree = ""; }; - 6FB2A6782C2C5B9E004D20C8 /* EmptyState */ = { - isa = PBXGroup; - children = ( - 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */, - 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */, - 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */, - ); - name = EmptyState; - sourceTree = ""; - }; 6FD1BAE02B87A0E8000C475C /* AdAttribution */ = { isa = PBXGroup; children = ( @@ -3997,7 +3981,6 @@ 6F691CC82C4979DD002E9553 /* Tooltip */, 6FA343902C3D3C2500470677 /* Item */, 6FA3438D2C3D3BB800470677 /* Model */, - 6FB2A6782C2C5B9E004D20C8 /* EmptyState */, 6FE1273C2C204C2500EB5724 /* FavoritesView.swift */, ); name = Favorites; @@ -7356,7 +7339,6 @@ 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */, 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */, F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, - 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, @@ -7436,7 +7418,7 @@ 6F9FFE302C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, - 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */, + 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */, 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, @@ -7589,7 +7571,6 @@ 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, - 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckPlayerNavigationHandling.swift in Sources */, 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, @@ -7620,7 +7601,7 @@ D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, C1641EB12BC2F52B0012607A /* ImportPasswordsView.swift in Sources */, CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */, - 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */, + 6FDC64052C98515E00DB71B3 /* FavoriteAddItemView.swift in Sources */, 982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */, F446B9B5251150AC00324016 /* HomeMessageViewSectionRenderer.swift in Sources */, D6E0C1852B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift in Sources */, @@ -7710,7 +7691,7 @@ 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */, + 6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -7872,7 +7853,6 @@ D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, - 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */, 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */, F17669D71E43401C003D3222 /* MainViewController.swift in Sources */, 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */, @@ -9170,7 +9150,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9207,7 +9187,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9297,7 +9277,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9324,7 +9304,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9473,7 +9453,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9498,7 +9478,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9567,7 +9547,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9601,7 +9581,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9634,7 +9614,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9664,7 +9644,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9974,7 +9954,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10005,7 +9985,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10033,7 +10013,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10066,7 +10046,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10096,7 +10076,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10129,11 +10109,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 4; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10366,7 +10346,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10393,7 +10373,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10425,7 +10405,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10462,7 +10442,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10497,7 +10477,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10532,11 +10512,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 4; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10709,11 +10689,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 4; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10742,10 +10722,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 4; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10955,7 +10935,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 198.1.0; + version = 198.2.1; }; }; 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 d8b3ac6978..9f70b65f0c 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" : "4db50292abf1180d66da55cf83f75d37395df1f9", - "version" : "198.1.0" + "revision" : "b60b38bace7262e0c4a006018b7e4b060ba4b754", + "version" : "198.2.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "2bed9e2963b2a9232452911d0773fac8b56416a1", - "version" : "6.17.0" + "revision" : "1ed569676555d493c9c5575eaed22aa02569aac9", + "version" : "6.19.0" } }, { diff --git a/DuckDuckGo/AddFavoritePlaceholderItemView.swift b/DuckDuckGo/FavoriteAddItemView.swift similarity index 89% rename from DuckDuckGo/AddFavoritePlaceholderItemView.swift rename to DuckDuckGo/FavoriteAddItemView.swift index 09b11802c0..85d9b92c1d 100644 --- a/DuckDuckGo/AddFavoritePlaceholderItemView.swift +++ b/DuckDuckGo/FavoriteAddItemView.swift @@ -1,5 +1,5 @@ // -// AddFavoritePlaceholderItemView.swift +// FavoriteAddItemView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -20,7 +20,7 @@ import SwiftUI import DesignResourcesKit -struct AddFavoritePlaceholderItemView: View { +struct FavoriteAddItemView: View { var body: some View { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(.clear) @@ -33,6 +33,6 @@ struct AddFavoritePlaceholderItemView: View { } #Preview { - AddFavoritePlaceholderItemView() + FavoriteAddItemView() .frame(width: 100) } diff --git a/DuckDuckGo/FavoriteItem.swift b/DuckDuckGo/FavoriteItem.swift index 9e6c433d53..0edefc1147 100644 --- a/DuckDuckGo/FavoriteItem.swift +++ b/DuckDuckGo/FavoriteItem.swift @@ -23,6 +23,7 @@ import UniformTypeIdentifiers enum FavoriteItem { case favorite(Favorite) case addFavorite + case placeholder(_ id: String) } extension FavoriteItem: Identifiable { @@ -32,6 +33,8 @@ extension FavoriteItem: Identifiable { return favorite.id case .addFavorite: return "addFavorite" + case .placeholder(let id): + return id } } } @@ -43,7 +46,7 @@ extension FavoriteItem: Reorderable { let itemProvider = NSItemProvider(object: (favorite.urlObject?.absoluteString ?? "") as NSString) let metadata = MoveMetadata(itemProvider: itemProvider, type: .plainText) return .movable(metadata) - case .addFavorite: + case .addFavorite, .placeholder: return .stationary } } diff --git a/DuckDuckGo/FavoriteEmptyStateItem.swift b/DuckDuckGo/FavoritePlaceholderItemView.swift similarity index 89% rename from DuckDuckGo/FavoriteEmptyStateItem.swift rename to DuckDuckGo/FavoritePlaceholderItemView.swift index e5b69afd8f..d009558b07 100644 --- a/DuckDuckGo/FavoriteEmptyStateItem.swift +++ b/DuckDuckGo/FavoritePlaceholderItemView.swift @@ -1,5 +1,5 @@ // -// FavoriteEmptyStateItem.swift +// FavoritePlaceholderItemView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -19,7 +19,7 @@ import SwiftUI -struct FavoriteEmptyStateItem: View { +struct FavoritePlaceholderItemView: View { var body: some View { RoundedRectangle(cornerRadius: 8, style: .continuous) .stroke(Color(designSystemColor: .lines), @@ -29,5 +29,5 @@ struct FavoriteEmptyStateItem: View { } #Preview { - FavoriteEmptyStateItem() + FavoritePlaceholderItemView() } diff --git a/DuckDuckGo/FavoritesDefaultViewModel.swift b/DuckDuckGo/FavoritesDefaultViewModel.swift deleted file mode 100644 index c9088dc5e7..0000000000 --- a/DuckDuckGo/FavoritesDefaultViewModel.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// FavoritesDefaultViewModel.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Bookmarks -import Combine -import SwiftUI -import Core -import WidgetKit - -protocol NewTabPageFavoriteDataSource { - var externalUpdates: AnyPublisher { get } - var favorites: [Favorite] { get } - - func moveFavorite(_ favorite: Favorite, - fromIndex: Int, - toIndex: Int) - - func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? - func favorite(at index: Int) throws -> Favorite? - func removeFavorite(_ favorite: Favorite) -} - -class FavoritesDefaultViewModel: FavoritesViewModel, FavoritesEmptyStateModel { - - @Published private(set) var allFavorites: [FavoriteItem] = [] - @Published private(set) var isCollapsed: Bool = true - @Published private(set) var isShowingTooltip: Bool = false - - private(set) var faviconLoader: FavoritesFaviconLoading? - - private var cancellables = Set() - - private let favoriteDataSource: NewTabPageFavoriteDataSource - private let pixelFiring: PixelFiring.Type - private let dailyPixelFiring: DailyPixelFiring.Type - - var isEmpty: Bool { - allFavorites.filter(\.isFavorite).isEmpty - } - - init(favoriteDataSource: NewTabPageFavoriteDataSource, - faviconLoader: FavoritesFaviconLoading, - pixelFiring: PixelFiring.Type = Pixel.self, - dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { - self.favoriteDataSource = favoriteDataSource - self.pixelFiring = pixelFiring - self.dailyPixelFiring = dailyPixelFiring - self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in - guard let self else { return } - - await MainActor.run { - self.faviconMissing() - } - }) - - - favoriteDataSource.externalUpdates.sink { [weak self] _ in - self?.updateData() - }.store(in: &cancellables) - - updateData() - } - - func toggleCollapse() { - isCollapsed.toggle() - - if isCollapsed { - pixelFiring.fire(.newTabPageFavoritesSeeLess, withAdditionalParameters: [:]) - } else { - pixelFiring.fire(.newTabPageFavoritesSeeMore, withAdditionalParameters: [:]) - } - } - - func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { - let maxCollapsedItemsCount = columnsCount * 2 - let favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites - let isCollapsible = allFavorites.count > maxCollapsedItemsCount - - return .init(items: favorites, isCollapsible: isCollapsible) - } - - // MARK: - External actions - - var onFaviconMissing: () -> Void = {} - func faviconMissing() { - onFaviconMissing() - } - - var onFavoriteURLSelected: ((URL) -> Void)? - func favoriteSelected(_ favorite: Favorite) { - guard let url = favorite.urlObject else { return } - - pixelFiring.fire(.favoriteLaunchedNTP, withAdditionalParameters: [:]) - dailyPixelFiring.fireDaily(.favoriteLaunchedNTPDaily) - Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) - - onFavoriteURLSelected?(url) - } - - var onFavoriteDeleted: ((BookmarkEntity) -> Void)? - func deleteFavorite(_ favorite: Favorite) { - guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } - - pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) - - favoriteDataSource.removeFavorite(favorite) - - WidgetCenter.shared.reloadAllTimelines() - updateData() - - onFavoriteDeleted?(entity) - } - - var onFavoriteEdit: ((BookmarkEntity) -> Void)? - func editFavorite(_ favorite: Favorite) { - guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } - - pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) - - onFavoriteEdit?(entity) - } - - func moveFavorites(from indexSet: IndexSet, to index: Int) { - guard indexSet.count == 1, - let fromIndex = indexSet.first else { return } - - let favoriteItem = allFavorites[fromIndex] - guard case let .favorite(favorite) = favoriteItem else { return } - - favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) - allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) - } - - // MARK: - Empty state model - - func placeholderTapped() { - pixelFiring.fire(.newTabPageFavoritesPlaceholderTapped, withAdditionalParameters: [:]) - } - - func toggleTooltip() { - isShowingTooltip.toggle() - if isShowingTooltip { - pixelFiring.fire(.newTabPageFavoritesInfoTooltip, withAdditionalParameters: [:]) - } - } - - // MARK: - - - private func updateData() { - var allFavorites = favoriteDataSource.favorites.map { - FavoriteItem.favorite($0) - } - allFavorites.append(.addFavorite) - - self.allFavorites = allFavorites - } -} - -enum FavoriteMappingError: Error { - case missingUUID -} - -private final class MissingFaviconWrapper: FavoritesFaviconLoading { - let loader: FavoritesFaviconLoading - - private(set) var onFaviconMissing: (() async -> Void) - - init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { - self.onFaviconMissing = onFaviconMissing - self.loader = loader - } - - func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { - let favicon = await loader.loadFavicon(for: favorite, size: size) - - if favicon == nil { - await onFaviconMissing() - } - - return favicon - } - - func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { - loader.fakeFavicon(for: favorite, size: size) - } - - func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { - loader.existingFavicon(for: favorite, size: size) - } -} - -private extension FavoriteItem { - var isFavorite: Bool { - switch self { - case .favorite: - return true - case .addFavorite: - return false - } - } -} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift deleted file mode 100644 index 8c81ce6765..0000000000 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// FavoritesEmptyStateView.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 DuckUI - -struct FavoritesEmptyStateView: View { - @ObservedObject var model: Model - @Binding var isAddingFavorite: Bool - - let geometry: GeometryProxy? - - var body: some View { - ZStack(alignment: .topTrailing) { - VStack(spacing: 16) { - FavoritesSectionHeader(model: model) - - NewTabPageGridView(geometry: geometry) { placeholdersCount in - Button(action: { - isAddingFavorite = true - }, label: { - AddFavoritePlaceholderItemView() - }) - .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) - .frame(width: NewTabPageGrid.Item.edgeSize) - - let placeholders = Array(0..: View { .frame(width: NewTabPageGrid.Item.edgeSize) .previewShape() .transition(.opacity) - case .addFavorite: + case .addFavorite, .placeholder: EmptyView() } } @@ -110,10 +110,17 @@ struct FavoritesView: View { Button(action: { isAddingFavorite = true }, label: { - AddFavoritePlaceholderItemView() + FavoriteAddItemView() }) .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) .frame(width: NewTabPageGrid.Item.edgeSize) + case .placeholder: + FavoritePlaceholderItemView() + .frame(width: NewTabPageGrid.Item.edgeSize, height: NewTabPageGrid.Item.edgeSize) + .contentShape(.rect) + .onTapGesture { + model.placeholderTapped() + } } } } diff --git a/DuckDuckGo/FavoritesViewModel.swift b/DuckDuckGo/FavoritesViewModel.swift index 624186edb6..781d77a044 100644 --- a/DuckDuckGo/FavoritesViewModel.swift +++ b/DuckDuckGo/FavoritesViewModel.swift @@ -18,37 +18,204 @@ // import Foundation +import Bookmarks +import Combine +import SwiftUI +import Core +import WidgetKit -protocol FavoritesViewModel: AnyObject, ObservableObject { - var allFavorites: [FavoriteItem] { get } - var faviconLoader: FavoritesFaviconLoading? { get } +protocol NewTabPageFavoriteDataSource { + var externalUpdates: AnyPublisher { get } + var favorites: [Favorite] { get } - var isEmpty: Bool { get } - var isCollapsed: Bool { get } + func moveFavorite(_ favorite: Favorite, + fromIndex: Int, + toIndex: Int) - func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice + func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? + func favorite(at index: Int) throws -> Favorite? + func removeFavorite(_ favorite: Favorite) +} + +struct FavoritesSlice { + let items: [FavoriteItem] + let isCollapsible: Bool +} + +class FavoritesViewModel: ObservableObject { + + @Published private(set) var allFavorites: [FavoriteItem] = [] + @Published private(set) var isCollapsed: Bool = true + + private(set) var faviconLoader: FavoritesFaviconLoading? + + private var cancellables = Set() + + private let favoriteDataSource: NewTabPageFavoriteDataSource + private let pixelFiring: PixelFiring.Type + private let dailyPixelFiring: DailyPixelFiring.Type + + var isEmpty: Bool { + allFavorites.filter(\.isFavorite).isEmpty + } + + init(favoriteDataSource: NewTabPageFavoriteDataSource, + faviconLoader: FavoritesFaviconLoading, + pixelFiring: PixelFiring.Type = Pixel.self, + dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { + self.favoriteDataSource = favoriteDataSource + self.pixelFiring = pixelFiring + self.dailyPixelFiring = dailyPixelFiring + self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in + guard let self else { return } + + await MainActor.run { + self.faviconMissing() + } + }) + + + favoriteDataSource.externalUpdates.sink { [weak self] _ in + self?.updateData() + }.store(in: &cancellables) + + updateData() + } + + func toggleCollapse() { + isCollapsed.toggle() + + if isCollapsed { + pixelFiring.fire(.newTabPageFavoritesSeeLess, withAdditionalParameters: [:]) + } else { + pixelFiring.fire(.newTabPageFavoritesSeeMore, withAdditionalParameters: [:]) + } + } + + func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + let hasFavorites = allFavorites.contains(where: \.isFavorite) + let maxCollapsedItemsCount = hasFavorites ? columnsCount * 2 : columnsCount + let isCollapsible = allFavorites.count > maxCollapsedItemsCount + + var favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites + + if !hasFavorites { + for _ in favorites.count ..< maxCollapsedItemsCount { + favorites.append(.placeholder(UUID().uuidString)) + } + } + + return .init(items: favorites, isCollapsible: isCollapsible) + } + + // MARK: - External actions + + var onFaviconMissing: () -> Void = {} + func faviconMissing() { + onFaviconMissing() + } - func faviconMissing() + var onFavoriteURLSelected: ((URL) -> Void)? + func favoriteSelected(_ favorite: Favorite) { + guard let url = favorite.urlObject else { return } - // MARK: - Interactions + pixelFiring.fire(.favoriteLaunchedNTP, withAdditionalParameters: [:]) + dailyPixelFiring.fireDaily(.favoriteLaunchedNTPDaily) + Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) - func toggleCollapse() + onFavoriteURLSelected?(url) + } - func favoriteSelected(_ favorite: Favorite) - func editFavorite(_ favorite: Favorite) - func deleteFavorite(_ favorite: Favorite) - func moveFavorites(from indexSet: IndexSet, to index: Int) + var onFavoriteDeleted: ((BookmarkEntity) -> Void)? + func deleteFavorite(_ favorite: Favorite) { + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } + + pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) + + favoriteDataSource.removeFavorite(favorite) + + WidgetCenter.shared.reloadAllTimelines() + updateData() + + onFavoriteDeleted?(entity) + } + + var onFavoriteEdit: ((BookmarkEntity) -> Void)? + func editFavorite(_ favorite: Favorite) { + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } + + pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) + + onFavoriteEdit?(entity) + } + + func moveFavorites(from indexSet: IndexSet, to index: Int) { + guard indexSet.count == 1, + let fromIndex = indexSet.first else { return } + + let favoriteItem = allFavorites[fromIndex] + guard case let .favorite(favorite) = favoriteItem else { return } + + favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) + allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) + } + + func placeholderTapped() { + pixelFiring.fire(.newTabPageFavoritesPlaceholderTapped, withAdditionalParameters: [:]) + } + + // MARK: - + + private func updateData() { + var allFavorites = favoriteDataSource.favorites.map { + FavoriteItem.favorite($0) + } + allFavorites.append(.addFavorite) + + self.allFavorites = allFavorites + } +} + +enum FavoriteMappingError: Error { + case missingUUID } -protocol FavoritesEmptyStateModel: AnyObject, ObservableObject { +private final class MissingFaviconWrapper: FavoritesFaviconLoading { + let loader: FavoritesFaviconLoading + + private(set) var onFaviconMissing: (() async -> Void) + + init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { + self.onFaviconMissing = onFaviconMissing + self.loader = loader + } - var isShowingTooltip: Bool { get } + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + let favicon = await loader.loadFavicon(for: favorite, size: size) - func placeholderTapped() - func toggleTooltip() + if favicon == nil { + await onFaviconMissing() + } + + return favicon + } + + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + loader.fakeFavicon(for: favorite, size: size) + } + + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + loader.existingFavicon(for: favorite, size: size) + } } -struct FavoritesSlice { - let items: [FavoriteItem] - let isCollapsible: Bool +private extension FavoriteItem { + var isFavorite: Bool { + switch self { + case .favorite: + return true + case .addFavorite, .placeholder: + return false + } + } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index 14d97ce3e2..aaed282ca5 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -21,12 +21,12 @@ import SwiftUI import DuckUI import RemoteMessaging -struct NewTabPageView: View { +struct NewTabPageView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject private var viewModel: NewTabPageViewModel @ObservedObject private var messagesModel: NewTabPageMessagesModel - @ObservedObject private var favoritesModel: FavoritesModelType + @ObservedObject private var favoritesViewModel: FavoritesViewModel @ObservedObject private var shortcutsModel: ShortcutsModel @ObservedObject private var shortcutsSettingsModel: NewTabPageShortcutsSettingsModel @ObservedObject private var sectionsSettingsModel: NewTabPageSectionsSettingsModel @@ -36,13 +36,13 @@ struct NewTabPageView some View { - Group { - if favoritesModel.isEmpty { - FavoritesEmptyStateView(model: favoritesModel, - isAddingFavorite: $isAddingFavorite, - geometry: proxy) - .padding(.top, Metrics.nonGridSectionTopPadding) - } else { - FavoritesView(model: favoritesModel, + FavoritesView(model: favoritesViewModel, isAddingFavorite: $isAddingFavorite, geometry: proxy) - } - } } @ViewBuilder @@ -274,7 +260,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -299,7 +285,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { ] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -314,7 +300,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(favorites: []), + favoritesViewModel: FavoritesPreviewModel(favorites: []), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -329,7 +315,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel(storage: .emptyStorage()) diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index ffd3e9af70..ee3d0bb4a0 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -23,7 +23,7 @@ import Bookmarks import BrowserServicesKit import Core -final class NewTabPageViewController: UIHostingController>, NewTabPage { +final class NewTabPageViewController: UIHostingController, NewTabPage { private let syncService: DDGSyncing private let syncBookmarksAdapter: SyncBookmarksAdapter @@ -35,7 +35,7 @@ final class NewTabPageViewController: UIHostingController FavoritesDefaultViewModel { - FavoritesDefaultViewModel(favoriteDataSource: favoriteDataSource, - faviconLoader: FavoritesFaviconLoader(), - pixelFiring: PixelFiringMock.self, - dailyPixelFiring: PixelFiringMock.self) + func testPrefixFavoritesLimitsToTwoRows() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT() + + let slice = sut.prefixedFavorites(for: 4) + + XCTAssertEqual(slice.items.count, 8) + XCTAssertTrue(slice.isCollapsible) + } + + func testAddItemIsLastWhenFavoritesPresent() throws { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT() + + let lastItem = try XCTUnwrap(sut.allFavorites.last) + + XCTAssertTrue(lastItem == .addFavorite) + } + + func testAddItemIsFirstWhenFavoritesEmpty() throws { + let sut = createSUT() + + let firstItem = try XCTUnwrap(sut.allFavorites.first) + + XCTAssertTrue(firstItem == .addFavorite) + } + + private func createSUT() -> FavoritesViewModel { + FavoritesViewModel(favoriteDataSource: favoriteDataSource, + faviconLoader: FavoritesFaviconLoader(), + pixelFiring: PixelFiringMock.self, + dailyPixelFiring: PixelFiringMock.self) } } @@ -129,3 +157,12 @@ private extension Favorite { Favorite(id: UUID().uuidString, title: "foo", domain: "bar") } } + +private extension FavoriteItem { + var isPlaceholder: Bool { + switch self { + case .placeholder: return true + case .favorite, .addFavorite: return false + } + } +}