From 94cb8ba6d1ee4e14204fe4f13cffe13161339b41 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 15:37:08 +0100 Subject: [PATCH 01/24] Ensure migration has occurred before accessing vault --- .../CredentialProviderViewController.swift | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index 35f730611e..149447f243 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -22,6 +22,7 @@ import SwiftUI import BrowserServicesKit import Core import Common +import os.log class CredentialProviderViewController: ASCredentialProviderViewController { @@ -110,7 +111,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { installChildViewController(hostingController) Task { - await credentialIdentityStoreManager.populateCredentialStore() + if let self = self, self.findKeychainItemsWithV4() { + await credentialIdentityStoreManager.populateCredentialStore() + } } Pixel.fire(pixel: .autofillExtensionEnabled) @@ -203,4 +206,31 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } } } + + private func findKeychainItemsWithV4() -> Bool { + var itemsWithV4: [String] = [] + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecReturnAttributes as String: kCFBooleanTrue!, + kSecMatchLimit as String: kSecMatchLimitAll + ] + + var result: AnyObject? + + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecSuccess, let items = result as? [[String: Any]] { + for item in items { + if let service = item[kSecAttrService as String] as? String, + service.contains("v4") { + itemsWithV4.append(service) + } + } + } else { + Logger.autofill.debug("No items found or error: \(status)") + } + + return !itemsWithV4.isEmpty + } } From 7bb73173badcd7f7f84d65841f9de459f03d5ad8 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 15:37:35 +0100 Subject: [PATCH 02/24] Update to target main app --- .../xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme index 02ee5661e7..7708563eda 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme @@ -46,7 +46,7 @@ shouldAutocreateTestPlan = "YES"> Date: Wed, 18 Dec 2024 15:39:13 +0100 Subject: [PATCH 03/24] Populate credential store if user has enabled before launching app --- DuckDuckGo/AutofillUsageMonitor.swift | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/AutofillUsageMonitor.swift b/DuckDuckGo/AutofillUsageMonitor.swift index 5c9f9bf87f..6123c43180 100644 --- a/DuckDuckGo/AutofillUsageMonitor.swift +++ b/DuckDuckGo/AutofillUsageMonitor.swift @@ -19,19 +19,34 @@ import Core import AuthenticationServices +import BrowserServicesKit final class AutofillUsageMonitor { + private lazy var credentialIdentityStoreManager: AutofillCredentialIdentityStoreManager? = { + guard let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) else { + return nil + } + + return AutofillCredentialIdentityStoreManager(vault: vault, + tld: AppDependencyProvider.shared.storageCache.tld) + }() + init() { NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil) - ASCredentialIdentityStore.shared.getState({ state in + ASCredentialIdentityStore.shared.getState({ [weak self] state in if state.isEnabled { - self.autofillExtensionEnabled = true + if self?.autofillExtensionEnabled == nil { + Task { + await self?.credentialIdentityStoreManager?.populateCredentialStore() + } + } + self?.autofillExtensionEnabled = true } else { - if self.autofillExtensionEnabled != nil { + if self?.autofillExtensionEnabled != nil { Pixel.fire(pixel: .autofillExtensionDisabled) - self.autofillExtensionEnabled = false + self?.autofillExtensionEnabled = false } } }) From f391cce59bd35a12b9f5c763c8de2211946f7b4c Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 15:52:34 +0100 Subject: [PATCH 04/24] Ensure migration has occurred before accessing vault --- .../CredentialProvider/CredentialProviderViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index 149447f243..3017132267 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -111,7 +111,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { installChildViewController(hostingController) Task { - if let self = self, self.findKeychainItemsWithV4() { + if findKeychainItemsWithV4() { await credentialIdentityStoreManager.populateCredentialStore() } } From da651c8ed27fd9f622389b8778300e2619a911d1 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 17:04:06 +0100 Subject: [PATCH 05/24] An additional protective in case users try to access the passwords list via the extension, before launching the app --- .../CredentialProviderViewController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index 3017132267..557b7292e7 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -38,7 +38,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { tld: tld) private lazy var secureVault: (any AutofillSecureVault)? = { - try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) + if findKeychainItemsWithV4() { + return try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) + } else { + return nil + } }() private lazy var tld: TLD = TLD() From 1e0fa7a1c667176e72edcb1553975035f284dca6 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Wed, 18 Dec 2024 19:35:27 +0100 Subject: [PATCH 06/24] Release 7.149.1-0 (#3740) --- Configuration/Version.xcconfig | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 64 ++++++++++----------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- fastlane/metadata/default/release_notes.txt | 2 +- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 9ef4c72f39..fde561fa40 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.149.0 +MARKETING_VERSION = 7.149.1 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index fbe80bb961..2d0f1997f1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9670,7 +9670,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; @@ -9707,7 +9707,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; @@ -9797,7 +9797,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 = ( @@ -9824,7 +9824,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; @@ -9972,7 +9972,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; @@ -9998,7 +9998,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 = ( @@ -10065,7 +10065,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; @@ -10099,7 +10099,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; @@ -10132,7 +10132,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 = ( @@ -10162,7 +10162,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; @@ -10493,7 +10493,7 @@ CODE_SIGN_ENTITLEMENTS = AutofillCredentialProvider/AutofillCredentialProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -10531,7 +10531,7 @@ CODE_SIGN_ENTITLEMENTS = AutofillCredentialProvider/AutofillCredentialProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -10568,7 +10568,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; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -10607,7 +10607,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; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -10705,7 +10705,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; @@ -10736,7 +10736,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 = ( @@ -10764,7 +10764,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 = ( @@ -10797,7 +10797,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; @@ -10827,7 +10827,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; @@ -10860,11 +10860,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"; @@ -11096,7 +11096,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; @@ -11124,7 +11124,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; @@ -11156,7 +11156,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; @@ -11193,7 +11193,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; @@ -11228,7 +11228,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; @@ -11263,11 +11263,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"; @@ -11439,11 +11439,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"; @@ -11472,10 +11472,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 740cfbe9a5..5b6f693ce0 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.149.0 + 7.149.1 Key version Title diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 66b16c0810..58bb2b4a63 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1 +1 @@ -- Bug fixes and other improvements \ No newline at end of file + - If you use dark mode, you'll notice the app icon has an improved look, whether you've stuck with the classic orange or picked a custom icon color. \ No newline at end of file From 079e995dc8674920758dbd3c7a919ef6ce9006b4 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 15:37:08 +0100 Subject: [PATCH 07/24] Ensure migration has occurred before accessing vault --- .../CredentialProviderViewController.swift | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index 35f730611e..149447f243 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -22,6 +22,7 @@ import SwiftUI import BrowserServicesKit import Core import Common +import os.log class CredentialProviderViewController: ASCredentialProviderViewController { @@ -110,7 +111,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { installChildViewController(hostingController) Task { - await credentialIdentityStoreManager.populateCredentialStore() + if let self = self, self.findKeychainItemsWithV4() { + await credentialIdentityStoreManager.populateCredentialStore() + } } Pixel.fire(pixel: .autofillExtensionEnabled) @@ -203,4 +206,31 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } } } + + private func findKeychainItemsWithV4() -> Bool { + var itemsWithV4: [String] = [] + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecReturnAttributes as String: kCFBooleanTrue!, + kSecMatchLimit as String: kSecMatchLimitAll + ] + + var result: AnyObject? + + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecSuccess, let items = result as? [[String: Any]] { + for item in items { + if let service = item[kSecAttrService as String] as? String, + service.contains("v4") { + itemsWithV4.append(service) + } + } + } else { + Logger.autofill.debug("No items found or error: \(status)") + } + + return !itemsWithV4.isEmpty + } } From 1e4ecf0bcce25d5a4063be71c5cb22eceb023dc4 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 15:37:35 +0100 Subject: [PATCH 08/24] Update to target main app --- .../xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme index 02ee5661e7..7708563eda 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AutofillCredentialProvider.xcscheme @@ -46,7 +46,7 @@ shouldAutocreateTestPlan = "YES"> Date: Wed, 18 Dec 2024 15:39:13 +0100 Subject: [PATCH 09/24] Populate credential store if user has enabled before launching app --- DuckDuckGo/AutofillUsageMonitor.swift | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/AutofillUsageMonitor.swift b/DuckDuckGo/AutofillUsageMonitor.swift index 5c9f9bf87f..6123c43180 100644 --- a/DuckDuckGo/AutofillUsageMonitor.swift +++ b/DuckDuckGo/AutofillUsageMonitor.swift @@ -19,19 +19,34 @@ import Core import AuthenticationServices +import BrowserServicesKit final class AutofillUsageMonitor { + private lazy var credentialIdentityStoreManager: AutofillCredentialIdentityStoreManager? = { + guard let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) else { + return nil + } + + return AutofillCredentialIdentityStoreManager(vault: vault, + tld: AppDependencyProvider.shared.storageCache.tld) + }() + init() { NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil) - ASCredentialIdentityStore.shared.getState({ state in + ASCredentialIdentityStore.shared.getState({ [weak self] state in if state.isEnabled { - self.autofillExtensionEnabled = true + if self?.autofillExtensionEnabled == nil { + Task { + await self?.credentialIdentityStoreManager?.populateCredentialStore() + } + } + self?.autofillExtensionEnabled = true } else { - if self.autofillExtensionEnabled != nil { + if self?.autofillExtensionEnabled != nil { Pixel.fire(pixel: .autofillExtensionDisabled) - self.autofillExtensionEnabled = false + self?.autofillExtensionEnabled = false } } }) From 3c2509bb8ef525ad1b026207dc874aa97f056974 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 15:52:34 +0100 Subject: [PATCH 10/24] Ensure migration has occurred before accessing vault --- .../CredentialProvider/CredentialProviderViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index 149447f243..3017132267 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -111,7 +111,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { installChildViewController(hostingController) Task { - if let self = self, self.findKeychainItemsWithV4() { + if findKeychainItemsWithV4() { await credentialIdentityStoreManager.populateCredentialStore() } } From 3372bc8329ef8c72d9ee8418e4b32d7dd69a43a8 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Wed, 18 Dec 2024 17:04:06 +0100 Subject: [PATCH 11/24] An additional protective in case users try to access the passwords list via the extension, before launching the app --- .../CredentialProviderViewController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index 3017132267..557b7292e7 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -38,7 +38,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { tld: tld) private lazy var secureVault: (any AutofillSecureVault)? = { - try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) + if findKeychainItemsWithV4() { + return try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) + } else { + return nil + } }() private lazy var tld: TLD = TLD() From 577e08caf8fc90fdeea344909d45660949ded8e4 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Wed, 18 Dec 2024 22:02:01 +0100 Subject: [PATCH 12/24] Release 7.150.0-1 (#3742) --- DuckDuckGo.xcodeproj/project.pbxproj | 64 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4ff434240a..c655af07ad 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9670,7 +9670,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; @@ -9707,7 +9707,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; @@ -9797,7 +9797,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 = ( @@ -9824,7 +9824,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; @@ -9972,7 +9972,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; @@ -9998,7 +9998,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 = ( @@ -10065,7 +10065,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; @@ -10099,7 +10099,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; @@ -10132,7 +10132,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 = ( @@ -10162,7 +10162,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; @@ -10493,7 +10493,7 @@ CODE_SIGN_ENTITLEMENTS = AutofillCredentialProvider/AutofillCredentialProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -10531,7 +10531,7 @@ CODE_SIGN_ENTITLEMENTS = AutofillCredentialProvider/AutofillCredentialProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -10568,7 +10568,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; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -10607,7 +10607,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; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -10705,7 +10705,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; @@ -10736,7 +10736,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 = ( @@ -10764,7 +10764,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 = ( @@ -10797,7 +10797,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; @@ -10827,7 +10827,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; @@ -10860,11 +10860,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"; @@ -11096,7 +11096,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; @@ -11124,7 +11124,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; @@ -11156,7 +11156,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; @@ -11193,7 +11193,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; @@ -11228,7 +11228,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; @@ -11263,11 +11263,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"; @@ -11439,11 +11439,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"; @@ -11472,10 +11472,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 6d1fefb7a98e404f45db6579cfa5655ee3863d4a Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 18 Dec 2024 13:02:10 -0800 Subject: [PATCH 13/24] Remove ESLint config files (#3739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1208971717942902/f Tech Design URL: CC: **Description**: This PR cleans up unused ESLint configuration files. **Steps to test this PR**: 1. Check that I haven't missed removing any other ESLint files **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **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) --- .eslintignore | 1 - .eslintrc | 25 ------------------------- 2 files changed, 26 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 539b4cdd5c..0000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -submodules/ diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3b909f8fd4..0000000000 --- a/.eslintrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "env": { - "browser": true, - "es2017": true - }, - "extends": [ - "standard" - ], - "globals": { - "__firefox__": "readonly", - "SECURITY_TOKEN": "readonly", - "$FEATURE_SETTINGS$": "readonly", - "$GPC_ENABLED$": "readonly", - "$BLOCKING_ENABLED$": "readonly", - "$TRACKER_DATA$": "readonly", - "$IS_DEBUG$": "readonly", - "webkit": "readonly" - }, - "parserOptions": { - "ecmaVersion": 7 - }, - "rules": { - "indent": ["error", 4] - } -} From cdeddbadab176bbd3d9ed63c091d0ff942017070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Thu, 19 Dec 2024 10:15:05 +0100 Subject: [PATCH 14/24] Fix BrowsingMenu layout (#3712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414709148257752/1208946169184139/f Tech Design URL: CC: **Description**: Adjusts layout for `BrowsingMenuButton` so that it expands along with the label inside. Additionally `BrowsingMenuViewController` got a new constraint preventing the menu view from going outside view bounds. **Steps to test this PR**: 1. Enable link conditioner with a bad/slow connection profile. 2. Try to load any site, stop before it finishes loading. 3. Open menu, check if button titles are visible. 4. Load site again (without link conditioner). 5. Check browsing menu sizing and layout is as expected. 6. Go to System Settings -> Developer -> View (simulator) or Display & Brightness -> Display Zoom (physical device) and set display zoom to "Larger text". 7. Repeat testing steps. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [x] iPhone SE (1st Gen) * [x] iPhone 8 * [ ] iPhone X * [x] iPhone 14 Pro * [x] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- .../BrowsingMenu/BrowsingMenuButton.xib | 39 ++++++------ .../BrowsingMenuViewController.storyboard | 60 +++++++++---------- ...bViewControllerBrowsingMenuExtension.swift | 5 +- 3 files changed, 55 insertions(+), 49 deletions(-) diff --git a/DuckDuckGo/BrowsingMenu/BrowsingMenuButton.xib b/DuckDuckGo/BrowsingMenu/BrowsingMenuButton.xib index 6936ad96a9..358b28ef66 100644 --- a/DuckDuckGo/BrowsingMenu/BrowsingMenuButton.xib +++ b/DuckDuckGo/BrowsingMenu/BrowsingMenuButton.xib @@ -1,39 +1,36 @@ - + - + - - + + - + - + - diff --git a/DuckDuckGo/BrowsingMenu/BrowsingMenuViewController.storyboard b/DuckDuckGo/BrowsingMenu/BrowsingMenuViewController.storyboard index c15ecff942..9b439b5901 100644 --- a/DuckDuckGo/BrowsingMenu/BrowsingMenuViewController.storyboard +++ b/DuckDuckGo/BrowsingMenu/BrowsingMenuViewController.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -12,10 +13,6 @@ - - - - @@ -28,10 +25,10 @@ - + - + @@ -39,43 +36,44 @@ - + - + - - + + - - + + + - + - + - + - + - + @@ -87,7 +85,7 @@ - + @@ -133,20 +131,20 @@ - + - + - + - + @@ -196,18 +194,20 @@ + - + - + + - + - - + + diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index fe6b117e86..4dc0613771 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -128,7 +128,10 @@ extension TabViewController { })) } - entries.append(.separator) + // Do not add separator if there are no entries so far + if entries.count > 0 { + entries.append(.separator) + } let shortcutsEntries = buildShortcutsEntries(includeBookmarks: false) entries.append(contentsOf: shortcutsEntries) From 8a8f641298aff53fccfd06d3e3d939260a2a2055 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 19 Dec 2024 18:22:49 +0100 Subject: [PATCH 15/24] =?UTF-8?q?DuckPlayer:=20Don=E2=80=99t=20open=20new?= =?UTF-8?q?=20tabs=20or=20DuckPlayer=20at=20launch=20when=20in=20alwaysAsk?= =?UTF-8?q?=20mode=20(#3738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204099484721401/1208896356321073/f Tech Design URL: CC: **Description**: Bugfix: When launching the app after a crash or force close with DuckPlayer in 'Ask' mode, the app automatically opened `youtube.com/watch` pages (videos) in DuckPlayer when it shouldn't. This was caused by DuckPlayer's 'open in new tab' feature. When the app was first launched, a new tab opened for every `youtube.com/watch` page, which triggered DuckPlayer by default. --- DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift index 13afd90eb0..8ea855cfca 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift @@ -954,7 +954,7 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling { // Redirect to Youtube + DuckPlayer Overlay if Ask Mode if url.isYoutubeWatch && duckPlayerMode == .alwaysAsk && !isDuckPlayerRedirect(url: url) { - redirectToYouTubeVideo(url: url, webView: webView, allowFirstVideo: false) + redirectToYouTubeVideo(url: url, webView: webView, allowFirstVideo: false, disableNewTab: true) return true } From df948c9c2a7e1a9004b36bc15dcdc7ccdf8eec60 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Thu, 19 Dec 2024 20:28:00 +0100 Subject: [PATCH 16/24] Fix sharing via the Share & Action extensions on iOS 18 (#3745) Task/Issue URL: https://app.asana.com/0/414709148257752/1208991975879543/f Tech Design URL: CC: **Description**: Fixes sharing to the DDG app from the share + action extensions on iOS 18 --- OpenAction/ActionViewController.swift | 13 ++++++++++--- ShareExtension/ShareViewController.swift | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/OpenAction/ActionViewController.swift b/OpenAction/ActionViewController.swift index 2a4c493ebf..afe78b94a4 100644 --- a/OpenAction/ActionViewController.swift +++ b/OpenAction/ActionViewController.swift @@ -64,9 +64,16 @@ class ActionViewController: UIViewController { var responder = self as UIResponder? let selectorOpenURL = sel_registerName("openURL:") while let current = responder { - if current.responds(to: selectorOpenURL) { - current.perform(selectorOpenURL, with: url, afterDelay: 0) - break + if #available(iOS 18.0, *) { + if let application = current as? UIApplication { + application.open(url, options: [:], completionHandler: nil) + break + } + } else { + if current.responds(to: selectorOpenURL) { + current.perform(selectorOpenURL, with: url, afterDelay: 0) + break + } } responder = current.next } diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift index 98829b4cbe..5afada430c 100644 --- a/ShareExtension/ShareViewController.swift +++ b/ShareExtension/ShareViewController.swift @@ -84,9 +84,16 @@ class ShareViewController: SLComposeServiceViewController { let deepLink = URL(string: AppDeepLinkSchemes.quickLink.appending(url.absoluteString))! var responder = self as UIResponder? while responder != nil { - if responder!.responds(to: selector) { - _ = responder?.perform(selector, with: deepLink, with: {}) - break + if #available(iOS 18.0, *) { + if let application = responder as? UIApplication { + application.open(deepLink, options: [:], completionHandler: nil) + break + } + } else { + if responder!.responds(to: selector) { + _ = responder?.perform(selector, with: deepLink, with: {}) + break + } } responder = responder!.next } From 8ce3815b51b25209cdef74eddc5e99d7e7b73dc3 Mon Sep 17 00:00:00 2001 From: Jonathan Jackson Date: Thu, 19 Dec 2024 16:25:37 -0500 Subject: [PATCH 17/24] Crash report cohort ID support for iOS (#3692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1208592102886666/1208759541597499/f Tech Design URL: https://app.asana.com/0/1208592102886666/1208660326715650/f **Description**: DO NOT MERGE - this is a draft for input, not ready to go live yet. iOS client support for CRCID send/receive (primarily supported in BSK, with changes under review in [BSK #1116](https://github.com/duckduckgo/BrowserServicesKit/pull/1116)). This is pretty straightforward, just conforming to CrashCollection’s new init signature, and clearing CRCIDs when the user opts out of crash reporting. BSK handles everything else. **Steps to test this PR**: Note: Must be tested on a physical device, as the simulator does not produce crash logs (and thus doesn’t find and upload them either). To cause and report a crash: 1. Launch the app and force a crash, which can be done from Settings → All Debug Options → Crash (fatal error) or similar. Note that Crash (CPU/Memory) does not appear to produce a crash log, and thus won’t trigger crash uploading. 2. Launch the app again (easiest with a debugger) 1. For the first crash of an app install: You will be prompted to opt in or out of crash reporting when the app is launched. Opt in and watch logs for “crcid” and you should see logs from CrashReportSender:56, and CrashCollection:95-109. 2. On subsequent crashes, when opted in, you should see statements confirming the received crcid was sent, and that the server returned either the same matching one, or a new one (in which case the new one should be stored and used on subsequent crash reports) To test clearing of the crcid when opting out: 1. Navigate to Settings → About and switch “Send Crash Reports” off, then back on again (this step should clear the crcid) 2. Follow steps from “To cause and report a crash” above, and confirm that the crash is submitted without an initial crcid, and that the server assigns one and it is stored (causing and uploading a second crash should confirm this new value is used on send). --- Core/PixelEvent.swift | 5 +++ DuckDuckGo.xcodeproj/project.pbxproj | 6 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/AppDelegate.swift | 4 +- DuckDuckGo/AppUserDefaults.swift | 2 +- DuckDuckGo/CrashCollectionOnboarding.swift | 3 ++ .../CrashCollectionOnboardingViewModel.swift | 6 +++ DuckDuckGo/CrashReportSenderExtensions.swift | 40 +++++++++++++++++++ DuckDuckGo/SettingsViewModel.swift | 5 +++ DuckDuckGoTests/AppSettingsMock.swift | 1 - 10 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 DuckDuckGo/CrashReportSenderExtensions.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 18c6b0b761..0adf763dec 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -526,6 +526,9 @@ extension Pixel { case dbCrashDetectedDaily case crashOnCrashHandlersSetUp + case crashReportCRCIDMissing + case crashReportingSubmissionFailed + case dbMigrationError case dbRemovalError case dbDestroyError @@ -1456,6 +1459,8 @@ extension Pixel.Event { case .dbCrashDetected: return "m_d_crash" case .dbCrashDetectedDaily: return "m_d_crash_daily" + case .crashReportCRCIDMissing: return "m_crashreporting_crcid-missing" + case .crashReportingSubmissionFailed: return "m_crashreporting_submission-failed" case .crashOnCrashHandlersSetUp: return "m_d_crash_on_handlers_setup" case .dbMigrationError: return "m_d_dbme" case .dbRemovalError: return "m_d_dbre" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ed776495b7..91b6c160d0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -225,6 +225,7 @@ 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FCAABB2992F592000E420A /* MultilineScrollableTextFix.swift */; }; 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FCAABF29930E26000E420A /* FailedAssertionView.swift */; }; 37FD780F2A29E28B00B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD780E2A29E28B00B36DB1 /* SyncErrorHandler.swift */; }; + 46DD3D5A2D0A29F600F33D49 /* CrashReportSenderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DD3D592D0A29F400F33D49 /* CrashReportSenderExtensions.swift */; }; 4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */; }; 4B0F3F502B9BFF2100392892 /* NetworkProtectionFAQView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */; }; 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */; }; @@ -1597,6 +1598,7 @@ 37FCAABF29930E26000E420A /* FailedAssertionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAssertionView.swift; sourceTree = ""; }; 37FCAACB2993149A000E420A /* Waitlist */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Waitlist; sourceTree = ""; }; 37FD780E2A29E28B00B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; + 46DD3D592D0A29F400F33D49 /* CrashReportSenderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportSenderExtensions.swift; sourceTree = ""; }; 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationDebugViewController.swift; sourceTree = ""; }; 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFAQView.swift; sourceTree = ""; }; 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWidgetRefreshModel.swift; sourceTree = ""; }; @@ -3888,6 +3890,7 @@ 37CF915E2BB4735F00BADCAE /* Crashes */ = { isa = PBXGroup; children = ( + 46DD3D592D0A29F400F33D49 /* CrashReportSenderExtensions.swift */, 37CF915F2BB4737300BADCAE /* CrashCollectionOnboarding.swift */, 37CF91612BB474AA00BADCAE /* CrashCollectionOnboardingView.swift */, 37CF91632BB4A82A00BADCAE /* CrashCollectionOnboardingViewModel.swift */, @@ -8235,6 +8238,7 @@ BDF8D0022C1B87F4003E3B27 /* NetworkProtectionDNSSettingsViewModel.swift in Sources */, 9838059F2228208E00385F1A /* PositiveFeedbackViewController.swift in Sources */, 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, + 46DD3D5A2D0A29F600F33D49 /* CrashReportSenderExtensions.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, 317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */, @@ -11719,7 +11723,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 221.3.0; + version = 222.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 4eaad818db..5f7e2bbb19 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" : "b71ed70ce9b0ef3ce51d4f96da0193ab70493944", - "version" : "221.3.0" + "revision" : "5704d77e3b4c77c7387518d796d31a35f7a1ffcf", + "version" : "222.1.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 38ff685038..c176b05ae0 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -83,7 +83,9 @@ import os.log private var syncStateCancellable: AnyCancellable? private var isSyncInProgressCancellable: AnyCancellable? - private let crashCollection = CrashCollection(platform: .iOS) + private let crashCollection = CrashCollection(crashReportSender: CrashReportSender(platform: .iOS, + pixelEvents: CrashReportSender.pixelEvents), + crashCollectionStorage: UserDefaults()) private var crashReportUploaderOnboarding: CrashCollectionOnboarding? private var autofillPixelReporter: AutofillPixelReporter? diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 2c17e2ac1e..df880d42d6 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -76,7 +76,7 @@ public class AppUserDefaults: AppSettings { static let crashCollectionOptInStatus = "com.duckduckgo.ios.crashCollectionOptInStatus" static let crashCollectionShouldRevertOptedInStatusTrigger = "com.duckduckgo.ios.crashCollectionShouldRevertOptedInStatusTrigger" - + static let duckPlayerMode = "com.duckduckgo.ios.duckPlayerMode" static let duckPlayerAskModeOverlayHidden = "com.duckduckgo.ios.duckPlayerAskModeOverlayHidden" static let duckPlayerOpenInNewTab = "com.duckduckgo.ios.duckPlayerOpenInNewTab" diff --git a/DuckDuckGo/CrashCollectionOnboarding.swift b/DuckDuckGo/CrashCollectionOnboarding.swift index dd9e54af7c..7dac3c6265 100644 --- a/DuckDuckGo/CrashCollectionOnboarding.swift +++ b/DuckDuckGo/CrashCollectionOnboarding.swift @@ -55,9 +55,12 @@ final class CrashCollectionOnboarding: NSObject { func presentOnboardingIfNeeded(for payloads: [Data], from viewController: UIViewController, sendReport: @escaping () -> Void) { let isCurrentlyPresenting = viewController.presentedViewController != nil + // Note: DO NOT TURN THIS ON until updated screens for the opt-in prompt and screen for reviewing the kinds of data + // we collect are updated (project coming soon) if featureFlagger.isFeatureOn(.crashReportOptInStatusResetting) { if appSettings.crashCollectionOptInStatus == .optedIn && appSettings.crashCollectionShouldRevertOptedInStatusTrigger < crashCollectionShouldRevertOptedInStatusTriggerTargetValue { + appSettings.crashCollectionOptInStatus = .undetermined appSettings.crashCollectionShouldRevertOptedInStatusTrigger = crashCollectionShouldRevertOptedInStatusTriggerTargetValue } diff --git a/DuckDuckGo/CrashCollectionOnboardingViewModel.swift b/DuckDuckGo/CrashCollectionOnboardingViewModel.swift index bd641f257b..5badf39bee 100644 --- a/DuckDuckGo/CrashCollectionOnboardingViewModel.swift +++ b/DuckDuckGo/CrashCollectionOnboardingViewModel.swift @@ -19,6 +19,7 @@ import Foundation import SwiftUI +import Crashes final class CrashCollectionOnboardingViewModel: ObservableObject { @@ -106,6 +107,11 @@ final class CrashCollectionOnboardingViewModel: ObservableObject { } set { appSettings.crashCollectionOptInStatus = newValue + if appSettings.crashCollectionOptInStatus == .optedOut { + let crashCollection = CrashCollection.init(crashReportSender: CrashReportSender(platform: .iOS, + pixelEvents: CrashReportSender.pixelEvents)) + crashCollection.clearCRCID() + } } } } diff --git a/DuckDuckGo/CrashReportSenderExtensions.swift b/DuckDuckGo/CrashReportSenderExtensions.swift new file mode 100644 index 0000000000..a90e8d54da --- /dev/null +++ b/DuckDuckGo/CrashReportSenderExtensions.swift @@ -0,0 +1,40 @@ +// +// CrashReportSenderExtensions.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 Crashes +import Common +import Core + +extension CrashReportSender { + + static let pixelEvents: EventMapping = .init { event, _, _, _ in + switch event { + case CrashReportSenderError.crcidMissing: + Pixel.fire(pixel: .crashReportCRCIDMissing) + + case CrashReportSenderError.submissionFailed(let error): + if let error { + Pixel.fire(pixel: .crashReportingSubmissionFailed, + withAdditionalParameters: ["HTTPStatusCode": "\(error.statusCode)"]) + } else { + Pixel.fire(pixel: .crashReportingSubmissionFailed) + } + } + } +} diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index cfdc9ae43c..224f880c2c 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -25,6 +25,7 @@ import Common import Combine import SyncUI import DuckPlayer +import Crashes import Subscription import NetworkProtection @@ -377,6 +378,10 @@ final class SettingsViewModel: ObservableObject { Binding( get: { self.state.crashCollectionOptInStatus == .optedIn }, set: { + if self.appSettings.crashCollectionOptInStatus == .optedIn && $0 == false { + let crashCollection = CrashCollection(crashReportSender: CrashReportSender(platform: .iOS, pixelEvents: CrashReportSender.pixelEvents)) + crashCollection.clearCRCID() + } self.appSettings.crashCollectionOptInStatus = $0 ? .optedIn : .optedOut self.state.crashCollectionOptInStatus = $0 ? .optedIn : .optedOut } diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index 13ced3eb65..bfd5fff474 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -82,7 +82,6 @@ class AppSettingsMock: AppSettings { var autoconsentEnabled = true var crashCollectionOptInStatus: CrashCollectionOptInStatus = .undetermined - var crashCollectionShouldRevertOptedInStatusTrigger: Int = 0 var newTabPageSectionsEnabled: Bool = false From 9415d44f84d49f76b3123a12467b95fd58abbef2 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 19 Dec 2024 15:24:22 -0800 Subject: [PATCH 18/24] Upgrade to Xcode 16.2 (#3743) Task/Issue URL: https://app.asana.com/0/1199230911884351/1209011361031638/f Tech Design URL: CC: Description: This PR upgrades CI to Xcode 16.2. --- .xcode-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.xcode-version b/.xcode-version index c32b0ec5ab..f6eb05e3c6 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -16.1 +16.2 From 26699bebc4c4072ad19c5afda7e7813169c7a494 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Fri, 20 Dec 2024 08:28:02 +0100 Subject: [PATCH 19/24] Delegate new tab creation to WebView (#3746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/856498667320406/1208699273169079/f Tech Design URL: CC: **Description**: Delegate new tab creation to WebView. **Steps to test this PR**: 1. Navigate to https://privacy-test-pages.site/privacy-protections/referrer-trimming/ 2. Check for referrer and how tabs behave. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Device Testing**: * [ ] iPhone * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 * [ ] iOS 18 --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo/TabViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 19bb29eeec..031dd5ea55 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -2032,8 +2032,8 @@ extension TabViewController: WKNavigationDelegate { } if isNewTargetBlankRequest(navigationAction: navigationAction) { - delegate?.tab(self, didRequestNewTabForUrl: url, openedByPage: true, inheritingAttribution: adClickAttributionLogic.state) - completion(.cancel) + // This will fallback to native WebView handling through webView(_:createWebViewWith:for:windowFeatures:) + completion(allowPolicy) return } From c4ac58d9104364c018ade329fa9e6d995b2c727f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 20 Dec 2024 13:40:26 +0100 Subject: [PATCH 20/24] Add double background pixel with timestamps of possible interfering events (#3749) Task/Issue URL: https://app.asana.com/0/0/1208997399449398/f **Description**: Add double background pixel with timestamps of possibly interfering events --- Core/Pixel.swift | 10 ++++ Core/PixelEvent.swift | 2 + DuckDuckGo/AppConfigurationFetch.swift | 3 + DuckDuckGo/AppDelegate.swift | 25 ++++++-- .../AppLifecycle/AppStateTransitions.swift | 12 ++-- .../AppLifecycle/AppStates/Background.swift | 60 ++++++++++++++++++- DuckDuckGo/RemoteMessagingClient.swift | 5 +- 7 files changed, 103 insertions(+), 14 deletions(-) diff --git a/Core/Pixel.swift b/Core/Pixel.swift index ded3c22a2c..3f95d46454 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -165,6 +165,16 @@ public struct PixelParameters { public static let appState = "state" public static let appEvent = "event" + + public static let firstBackgroundTimestamp = "firstBackgroundTimestamp" + public static let secondBackgroundTimestamp = "secondBackgroundTimestamp" + public static let didReceiveMemoryWarningTimestamp = "didReceiveMemoryWarningTimestamp" + public static let didReceiveMXPayloadTimestamp = "didReceiveMXPayloadTimestamp" + public static let didReceiveUNNotification = "didReceiveUNNotification" + public static let didStartRemoteMessagingClientBackgroundTask = "didStartRemoteMessagingClientBackgroundTask" + public static let didStartAppConfigurationFetchBackgroundTask = "didStartAppConfigurationFetchBackgroundTask" + public static let didPerformFetchTimestamp = "didPerformFetchTimestamp" + public static let numberOfBackgrounds = "numberOfBackgrounds" } public struct PixelValues { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 0adf763dec..16091cf0c2 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -947,6 +947,7 @@ extension Pixel { // MARK: Lifecycle case appDidTransitionToUnexpectedState + case appDidConsecutivelyBackground } } @@ -1891,6 +1892,7 @@ extension Pixel.Event { // MARK: Lifecycle case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state" + case .appDidConsecutivelyBackground: return "m_debug_app-did-consecutively-background" } } diff --git a/DuckDuckGo/AppConfigurationFetch.swift b/DuckDuckGo/AppConfigurationFetch.swift index fcf1664d94..38d012b81e 100644 --- a/DuckDuckGo/AppConfigurationFetch.swift +++ b/DuckDuckGo/AppConfigurationFetch.swift @@ -85,6 +85,8 @@ class AppConfigurationFetch { return Date().timeIntervalSince(Self.lastConfigurationRefreshDate) > Constants.minimumConfigurationRefreshInterval } + static var didStartBackgroundTaskTimestamp: Date? + enum BackgroundRefreshCompletionStatus { case expired @@ -132,6 +134,7 @@ class AppConfigurationFetch { static func registerBackgroundRefreshTaskHandler() { BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundProcessingTaskIdentifier, using: nil) { (task) in + didStartBackgroundTaskTimestamp = Date() guard shouldRefresh else { task.setTaskCompleted(success: true) scheduleBackgroundRefreshTask() diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index c176b05ae0..e13e49f8f3 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -181,7 +181,8 @@ import os.log Configuration.setURLProvider(AppConfigurationURLProvider()) } - crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in + crashCollection.startAttachingCrashLogMessages { [weak self] pixelParameters, payloads, sendReport in + self?.didReceiveMXPayloadTimestamp = Date() pixelParameters.forEach { params in Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) @@ -198,11 +199,11 @@ import os.log // Async dispatch because rootViewController may otherwise be nil here DispatchQueue.main.async { - guard let viewController = self.window?.rootViewController else { return } + guard let viewController = self?.window?.rootViewController else { return } let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) - self.crashReportUploaderOnboarding = crashReportUploaderOnboarding + self?.crashReportUploaderOnboarding = crashReportUploaderOnboarding } } @@ -919,7 +920,7 @@ import os.log func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { Logger.lifecycle.debug(#function) - + didPerformFetchTimestamp = Date() AppConfigurationFetch().start(isBackgroundFetch: true) { result in switch result { case .noData: @@ -1166,6 +1167,21 @@ import os.log UIApplication.shared.shortcutItems = nil } } + + var didReceiveMemoryWarningTimestamp: Date? + func applicationDidReceiveMemoryWarning(_ application: UIApplication) { + didReceiveMemoryWarningTimestamp = Date() + } + var didReceiveMXPayloadTimestamp: Date? + var didReceiveUNNotificationTimestamp: Date? + var didStartRemoteMessagingClientBackgroundTaskTimestamp: Date? { + remoteMessagingClient.didStartBackgroundTaskTimestamp + } + var didStartAppConfigurationFetchBackgroundTaskTimestamp: Date? { + AppConfigurationFetch.didStartBackgroundTaskTimestamp + } + + var didPerformFetchTimestamp: Date? } extension AppDelegate: BlankSnapshotViewRecoveringDelegate { @@ -1218,6 +1234,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + didReceiveUNNotificationTimestamp = Date() if response.actionIdentifier == UNNotificationDefaultActionIdentifier { let identifier = response.notification.request.identifier diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index e7a5df5fbd..637e7b0a09 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -91,7 +91,7 @@ extension Background { case .openURL: return self case .backgrounding: - return DoubleBackground() + return DoubleBackground(previousDidEnterBackgroundTimestamp: timestamp, counter: 0) case .launching, .suspending: return handleUnexpectedEvent(event) } @@ -102,15 +102,14 @@ extension Background { extension DoubleBackground { func apply(event: AppEvent) -> any AppState { - // report event so we know what events can be called at this moment, but do not let SM be stuck in this state just not to be flooded with these events - _ = handleUnexpectedEvent(event) - switch event { case .activating(let application): return Active(application: application) case .suspending(let application): return Inactive(application: application) - case .launching, .backgrounding, .openURL: + case .backgrounding(let application): + return DoubleBackground(previousDidEnterBackgroundTimestamp: currentDidEnterBackgroundTimestamp, counter: counter) + case .launching, .openURL: return self } @@ -121,9 +120,6 @@ extension DoubleBackground { extension InactiveBackground { func apply(event: AppEvent) -> any AppState { - // report event so we know what events can be called at this moment, but do not let SM be stuck in this state just not to be flooded with these events - _ = handleUnexpectedEvent(event) - switch event { case .activating(let application): return Active(application: application) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 71b0ab4c1a..d3014c417e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -18,9 +18,12 @@ // import UIKit +import Core struct Background: AppState { + let timestamp = Date() + init(application: UIApplication) { } @@ -28,5 +31,60 @@ struct Background: AppState { } struct DoubleBackground: AppState { - + + private let dateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + return formatter + }() + + let currentDidEnterBackgroundTimestamp: Date + var counter: Int + + init(previousDidEnterBackgroundTimestamp: Date, counter: Int) { + self.currentDidEnterBackgroundTimestamp = Date() + self.counter = counter + 1 + + var parameters = [ + PixelParameters.firstBackgroundTimestamp: dateFormatter.string(from: previousDidEnterBackgroundTimestamp), + PixelParameters.secondBackgroundTimestamp: dateFormatter.string(from: currentDidEnterBackgroundTimestamp) + ] + + if counter < 5 { + parameters[PixelParameters.numberOfBackgrounds] = String(counter) + } + + func isValid(timestamp: Date) -> Bool { + timestamp >= previousDidEnterBackgroundTimestamp && timestamp <= currentDidEnterBackgroundTimestamp + } + + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + if let didReceiveMemoryWarningTimestamp = appDelegate.didReceiveMemoryWarningTimestamp, + isValid(timestamp: didReceiveMemoryWarningTimestamp) { + parameters[PixelParameters.didReceiveMemoryWarningTimestamp] = dateFormatter.string(from: didReceiveMemoryWarningTimestamp) + } + if let didReceiveMXPayloadTimestamp = appDelegate.didReceiveMXPayloadTimestamp, + isValid(timestamp: didReceiveMXPayloadTimestamp) { + parameters[PixelParameters.didReceiveMXPayloadTimestamp] = dateFormatter.string(from: didReceiveMXPayloadTimestamp) + } + if let didReceiveUNNotificationTimestamp = appDelegate.didReceiveUNNotificationTimestamp, + isValid(timestamp: didReceiveUNNotificationTimestamp) { + parameters[PixelParameters.didReceiveUNNotification] = dateFormatter.string(from: didReceiveUNNotificationTimestamp) + } + if let didStartRemoteMessagingClientBackgroundTaskTimestamp = appDelegate.didStartRemoteMessagingClientBackgroundTaskTimestamp, + isValid(timestamp: didStartRemoteMessagingClientBackgroundTaskTimestamp) { + parameters[PixelParameters.didStartRemoteMessagingClientBackgroundTask] = dateFormatter.string(from: didStartRemoteMessagingClientBackgroundTaskTimestamp) + } + if let didStartAppConfigurationFetchBackgroundTaskTimestamp = appDelegate.didStartAppConfigurationFetchBackgroundTaskTimestamp, + isValid(timestamp: didStartAppConfigurationFetchBackgroundTaskTimestamp) { + parameters[PixelParameters.didStartAppConfigurationFetchBackgroundTask] = dateFormatter.string(from: didStartAppConfigurationFetchBackgroundTaskTimestamp) + } + if let didPerformFetchTimestamp = appDelegate.didPerformFetchTimestamp, + isValid(timestamp: didPerformFetchTimestamp) { + parameters[PixelParameters.didPerformFetchTimestamp] = dateFormatter.string(from: didPerformFetchTimestamp) + } + } + Pixel.fire(pixel: .appDidConsecutivelyBackground, withAdditionalParameters: parameters) + } + } diff --git a/DuckDuckGo/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessagingClient.swift index 5425931ff8..3bc72438ea 100644 --- a/DuckDuckGo/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessagingClient.swift @@ -48,6 +48,8 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { let store: RemoteMessagingStoring let remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + var didStartBackgroundTaskTimestamp: Date? + convenience init( bookmarksDatabase: CoreDataDatabase, appSettings: AppSettings, @@ -113,7 +115,8 @@ extension RemoteMessagingClient { let remoteMessagingAvailabilityProvider = remoteMessagingAvailabilityProvider let store = store - BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundRefreshTaskIdentifier, using: nil) { task in + BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundRefreshTaskIdentifier, using: nil) { [weak self] task in + self?.didStartBackgroundTaskTimestamp = Date() guard Self.shouldRefresh else { task.setTaskCompleted(success: true) Self.scheduleBackgroundRefreshTask() From 141103226227c72325636a5603c6de7580ee1009 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 20 Dec 2024 10:43:38 -0300 Subject: [PATCH 21/24] iOS AI Chat - Improve show/dismiss animation (#3741) Task/Issue URL: https://app.asana.com/0/1204167627774280/1208991512395325/f **Description**: Allow user to drag the AI Chat view controller down by the title bar --- .../24px/AIChat-24.imageset/AIChat-24.pdf | Bin 1590 -> 4283 bytes DuckDuckGo/MainViewController.swift | 9 +- ...ndedPageSheetContainerViewController.swift | 135 +++++++----------- ...RoundedPageSheetPresentationAnimator.swift | 58 +++++++- DuckDuckGo/UserText.swift | 1 - DuckDuckGo/en.lproj/Localizable.strings | 3 - LocalPackages/AIChat/Package.swift | 6 + .../Public API/AIChatViewController.swift | 32 ++++- .../AIChat/Sources/AIChat/TitleBarView.swift | 90 ++++++++++++ .../AIChat/Sources/AIChat/UserText.swift | 2 +- 10 files changed, 236 insertions(+), 100 deletions(-) create mode 100644 LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/AIChat-24.imageset/AIChat-24.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/AIChat-24.imageset/AIChat-24.pdf index b806da0f5968053e90bf582df2ee923e9bbd50b3..731eeaddaa204972e4e3d503a098631dd39b7281 100644 GIT binary patch delta 3925 zcmZvfc|6o#+s93eWsvMUV@q~p873wBPD6;YGcxwwgy~BNS+ZmuyJU%E%{IbVqbw!+ z78NOFUz&LM-M`=cJU#b){yEoqpX;3KoIgJ2^*N0+t$ayr?5ghmC|^%|e-uirels1*QJLJrTRefQ^URCT}L&# z0NNo^4CON#dW>d|JDeqpY^A?=NAEy~+Zdj~ZRudCETWc9Fk~E1WVfgAJV!CWHx6}I zUO362WbY^OsI2rjA>^I74ZL;A+j(>jnoq5=E%99AF@jfp(34F?!ziBoeHhCKX>S45H})UM%O}@pu#v}J0i>mPPON#q~h#KmuB1xBV9ydn7IJDQo{P8 z6-L2|{3fAv(fNQsl$0l|`tz zMJ^-SI}iLi!5ek-W?Pku)M3k1svUti_pVH*4HAID~4a z)g?I&Zmz%w&HCjBcL(t)!u_26A^p|qw!*fefcbGc@yR*WPGopVhwQd_O0ypY;-n-ycF8VwcMrIO~^IPCBVhdb19)0! znzi0`ByEjn$0?W{k5euwE?h1Z7%d-L8_yb*AG0pR70;CY(s@+Y_df2O*y#EgZfxR% zZN)3qS2ic%AfPi0on+_TQP8)DFHE*?S~^c)A@~TG2{evgE)Fh^?qzaRSoU9bU2eT- z!T3}+Wq>br=IM*_T=ac()S86s#Ae7T8~4l zW};?XkC;Ao@X{bY{c5^x)wjx&D%Z*fRcV#KtNbcwDuI()Si{n^>Ex`nhUH3~%XZNR z&1#P~?uU)h9zUjxzo$=sf3aTs%i)KeRIS&GeD9H<7@snKHixlH)giUdX4>(^rOVEJ zfqjpR_KX}#WsQK#vN@?a*DpVkg5MglwW`Cs*PMu2Hu=OKP}17l#1oL#vex7?TQQf{ zvfjKs>oy1Ehh$;|gLAeppU|H~w%oTuhkYG9oC1Oi+d}bM_+Wfm+f7VH$hE!E-Prx| z{j9BqZR;O1Ytft3KWEkzx<5GiO$B@t0Vqxqj^{utVM$?IVWs*888PYg5>XjPQgbhN zmSiGOT_}lgXcBjm)KB9iV`T71^2`iX0v(8d+lQ z6%!O|=~^Ka5x#cKM~6hQM(YcR3&6EzV@w}0X*6mr6?GN47C*fiTs`~BSA1IXvQ)5l z+Gxc$vkCKO)kZehdkI92x7l^D%*>yesjz%Rc#i8^e{rQonZSpGA9)trSDLr=FpQzB z_h7)u-5h-@pIJDM|JL|2LKW#3bhQ z0Qha#?M$p+)!MYRTYmMn<^FQ;PVmOjb6VgsXQLiHkYyFzV3389{ZKG)pueqlgy>GWqxlgkZFcVT_QG>DGTx~D;l?~{x z3LOf0?N?6$Rqq>j`avH-10eKNO6`Y;BZ_@4qE#MJ8M$ORYEf?7{A}wt-+bXQVI!I| z(S=7uaciw?aTPDRQ1RK7@O8{_(lM(qZ#;5*huBZ7d2WtxEZ7_LS@KznU1XdKI3Exo zEN{dzPS{O3ioG#f>UpxVd&7;i7$~eTE~#0RQ>w3>9fKQ=cK z7OuCS9I1Ye$Xb%1gVAmCid+%JR&F*O#SWxumE(7aF%$i7O8KrwYd0550mBEsw=Fhg zU)PO&nI2y6_2``u4!!94%Hq%g_=cJu%o=FB#&l8^iXwi^xHfmvhG2%t@7>x7-mC6X zaP>-j)qtIm=a&0@VEwu^YyXhiY$KXGzfg!pbdt8%z$7-ehFUXP_cIP!+00{4{Y z96I-$N55BbfB()7r*Ugxy_P7T%kmEOHZd(bN0fe4eN53xIaf6!EA0daT#vmbvJ^pk_L(lxqOzyOZ|qSL?U)7usqv^Cj}uW#v7pbS~sWpVmb= zwCnUI2XyP^DRC!P_Qjh5$reyuSgC-~*!i&dCQZ1XzN%BVfMed!+jbZk$<1>e8AVf5 zSrt=LQvKpc0*SxgF*t#8Q~-kVbo$#Roz}k;c)31X9hYCti`#J)BJQ~e z$lTdjU@a^0Yyj@0dNa@55wP*UN>8D?QJ%+o*8^vWb=jT6otLfpU0nrrw5hqhUMJ{Y z{XxQ+S33PGEWRKOo0()_mT27l(Am*K)I!HV-{CIKF`Xh)UrUgVJ0{gE>Pg@#*FKq3 zWJQnzws(R`tC4)7XWfq84zCp$J%JPHk;wHE)NN-xvH-@Tq8YxwCK9q)hmE@M)PmH4 z(dQRh7EswzQxVics4iPlqq^;pq&SAPm zS=FehSN|M8oF-e(_)(Z_rtZlXGx+dp#vj}Coz`M$LiC&SPDFEt)x~e6v#UYUI!t$4 zKQ0D&e*&UyqOs|1fagLZ@63;rg`XZR8gEV(gFLc@i0^Psy-eSDBwP&aq2kA*&%RBqLqNQw%Ys7ZZ)osk-TT8)v}DKE8k9mO>(Ake|91>FVZk- z0iB!1^gmj9=V-5r?W@GH8Sz${eFVu^x9023rp!&kMjR@s0wP{URAUr&FbC)E$Y#Bd zbwsr|kK3`0c8F*1HMkdFM_+tube+T6URZNuuOPqsAr`mxf02w$! z0V;jgpq`MwdTAL28R`E(@~1`q%uTmRSNm+!P uZ0Z~sCUSP(vxfyLbpsuYg2JRBe|?1Qef=@MC})VAw6qLFNJ#U#*8czmKrRsg literal 1590 zcmY!laBkm(vTRHc6J~)rRJr8Ji%pz;v2WjoRZWc1%0<1ppRTqlk-zjA+Fc= zNzF?y$xtu`Dh~n*0Zk|_DN0Su<*H!ZI`wp(h@pV%_rIc>-MSuEP2X;FMCwP27~>KZ zp{^s}i|@|dXnT@Paj7Zy`ONFD!*168{`h--{O|AU=ii@iS93e>`ToC|zxUp)TXea9 zd9Ro4yx_e$dB?K$JU-pDDJ7_D{tPyBf?^i0aWy8`>+EvpsK3Ys+ zaekrvE#dbyF3&4^ml`4;{JeF)%k2TDd)wvPIw@_kZ=#ATlh5uIh-dpW$HruSNs7M& zf8D&x6SWe%cJ10_kfC?jHo6w5C{0|>$n*Mh!*LxEg&FpHYCb~+xFJ(rV*pa*f57*VR6z(_enVqbtYU9h1 zxY=>aJag7nV$J!wXO2na%02vL?K4-GqweI4RfozJ#%<8w{54LxI7`NCT0yO3=$qMV z;=QC8^^mRjk}LTKrPf@@_jvnReM9p!wuRLa>!U6%tA27}Re_OFj+|K2^ck%OO6E!5 ztrF6&zN+3jVYY_*dEVc#!n!Wy35V`jnXGI0&hArTpAc5`O5o9!Rf#*wF3&djTHM`G zv7jStnag(esHxE!HnSete4U!i#&q`Z?R%U&|B5+l?$e67NV-P<2dd%=DxqmTK@{u-HE zzh+k5J$UcN?(*ZZAH;L~mob2H3pBaI@(m~>KuQ5~69af|GJ%VN$_B^0y!?`4h3Hsl z9tx=}NLA1eNKA)iD&N$U%tWXB3WaE3c>yXdfaL|8VPpamLa<;a1*I0}mlh?b7At^C zBT$9}0)6NFypq&BppBqR4HFDVECPxtm_mh+vOY)%QIL6OrU0F)0P;bQ0?Z!g{8FG^ zLk#y9Ly7=cV3?tp1oB`p+(MAUARc#4EJ@7CPe;{SQIwj-WuRcr1@S&O2+T}PjZGDR z!cbrc3>C0M9!$u{5NIWekfDJIx++5hGhjfVsxkr^j3#7kU 0.3 || velocity.y > 1000 + if shouldDismiss { + interactiveDismissalTransition?.finish() + } else { + interactiveDismissalTransition?.cancel() + UIView.animate(withDuration: 0.2, animations: { + self.view.transform = .identity + }) + } + isInteractiveDismissal = false + interactiveDismissalTransition = nil + default: + break + } + } + + private func setupBackgroundView() { + view.addSubview(backgroundView) + + backgroundView.backgroundColor = .black + backgroundView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - titleBarView.heightAnchor.constraint(equalToConstant: 44) + backgroundView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } @@ -84,7 +108,7 @@ final class RoundedPageSheetContainerViewController: UIViewController { contentViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - contentViewController.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor), // Below the title bar + contentViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), contentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), contentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) @@ -95,6 +119,9 @@ final class RoundedPageSheetContainerViewController: UIViewController { contentViewController.view.clipsToBounds = true contentViewController.didMove(toParent: self) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + contentViewController.view.addGestureRecognizer(panGesture) } @objc func closeController() { @@ -110,70 +137,8 @@ extension RoundedPageSheetContainerViewController: UIViewControllerTransitioning func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return RoundedPageSheetDismissalAnimator() } -} - -final private class TitleBarView: UIView { - private let imageView: UIImageView - private let titleLabel: UILabel - private let closeButton: UIButton - - init(logoImage: UIImage?, title: String, closeAction: @escaping () -> Void) { - imageView = UIImageView(image: logoImage) - titleLabel = UILabel() - closeButton = UIButton(type: .system) - - super.init(frame: .zero) - - setupView(title: title, closeAction: closeAction) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView(title: String, closeAction: @escaping () -> Void) { - backgroundColor = .clear - - imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false - - let imageSize: CGFloat = 28 - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize) - ]) - - titleLabel.text = title - titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .semibold) - titleLabel.textColor = .white - titleLabel.translatesAutoresizingMaskIntoConstraints = false - - closeButton.setImage(UIImage(named: "Close-24"), for: .normal) - closeButton.tintColor = .white - closeButton.translatesAutoresizingMaskIntoConstraints = false - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - - addSubview(imageView) - addSubview(titleLabel) - addSubview(closeButton) - - NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), - imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - - titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 8), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - - closeButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), - closeButton.centerYAnchor.constraint(equalTo: centerYAnchor) - ]) - - self.closeAction = closeAction - } - - private var closeAction: (() -> Void)? - @objc private func closeButtonTapped() { - closeAction?() + func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + return isInteractiveDismissal ? interactiveDismissalTransition : nil } } diff --git a/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift b/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift index 8f5f584591..85cd7d8787 100644 --- a/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift +++ b/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift @@ -21,9 +21,11 @@ import UIKit enum AnimatorConstants { static let duration: TimeInterval = 0.4 + static let springDamping: CGFloat = 0.9 + static let springVelocity: CGFloat = 0.5 } -class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning { +final class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return AnimatorConstants.duration } @@ -39,7 +41,12 @@ class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTr toView.alpha = 0 contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height) - UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { + UIView.animate(withDuration: AnimatorConstants.duration, + delay: 0, + usingSpringWithDamping: AnimatorConstants.springDamping, + initialSpringVelocity: AnimatorConstants.springVelocity, + options: .curveEaseInOut, + animations: { toView.alpha = 1 contentView.transform = .identity }, completion: { finished in @@ -47,8 +54,9 @@ class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTr }) } } - class RoundedPageSheetDismissalAnimator: NSObject, UIViewControllerAnimatedTransitioning { + private var animator: UIViewPropertyAnimator? + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return AnimatorConstants.duration } @@ -58,14 +66,54 @@ class RoundedPageSheetDismissalAnimator: NSObject, UIViewControllerAnimatedTrans let fromView = fromViewController.view, let contentView = fromViewController.contentViewController.view else { return } + let fromBackgroundView = fromViewController.backgroundView let containerView = transitionContext.containerView - UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { - fromView.alpha = 0 + UIView.animate(withDuration: AnimatorConstants.duration, + delay: 0, + usingSpringWithDamping: AnimatorConstants.springDamping, + initialSpringVelocity: AnimatorConstants.springVelocity, + options: .curveEaseInOut, + animations: { + fromBackgroundView.alpha = 0 contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height) }, completion: { finished in fromView.removeFromSuperview() transitionContext.completeTransition(finished) }) } + + func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { + if let existingAnimator = animator { + return existingAnimator + } + + guard let fromViewController = transitionContext.viewController(forKey: .from) as? RoundedPageSheetContainerViewController, + let fromView = fromViewController.view, + let contentView = fromViewController.contentViewController.view else { + fatalError("Invalid view controller setup") + } + + let containerView = transitionContext.containerView + let fromBackgroundView = fromViewController.backgroundView + + let animator = UIViewPropertyAnimator(duration: AnimatorConstants.duration, + dampingRatio: AnimatorConstants.springDamping) { + fromBackgroundView.alpha = 0 + contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height) + } + + animator.addCompletion { position in + switch position { + case .end: + fromView.removeFromSuperview() + transitionContext.completeTransition(true) + default: + transitionContext.completeTransition(false) + } + } + + self.animator = animator + return animator + } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 86a2337975..c6e7a5722f 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1352,7 +1352,6 @@ But if you *do* want a peek under the hood, you can find more information about static let duckPlayerContingencyMessageCTA = NSLocalizedString("duck-player.video-contingency-cta", value: "Learn More", comment: "Button for the message explaining to the user that Duck Player is not available so the user can learn more") // MARK: - AI Chat - public static let aiChatTitle = NSLocalizedString("aichat.title", value: "DuckDuckGo AI Chat", comment: "Title for DuckDuckGo AI Chat. Should not be translated") public static let aiChatFeatureName = NSLocalizedString("aichat.settings.title", value: "AI Chat", comment: "Settings screen cell text for AI Chat settings") public static let aiChatSettingsEnableFooter = NSLocalizedString("aichat.settings.enable.footer", value: "Turning this off will hide the AI Chat feature in the DuckDuckGo app.", comment: "Footer text for AI Chat settings") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index d1bf6015ea..022fc631d0 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -158,9 +158,6 @@ /* Settings screen cell text for AI Chat settings */ "aichat.settings.title" = "AI Chat"; -/* Title for DuckDuckGo AI Chat. Should not be translated */ -"aichat.title" = "DuckDuckGo AI Chat"; - /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Existing bookmarks will not be duplicated."; diff --git a/LocalPackages/AIChat/Package.swift b/LocalPackages/AIChat/Package.swift index 478b4b4705..29f3abc3d5 100644 --- a/LocalPackages/AIChat/Package.swift +++ b/LocalPackages/AIChat/Package.swift @@ -13,9 +13,15 @@ let package = Package( targets: ["AIChat"] ), ], + dependencies: [ + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0") + ], targets: [ .target( name: "AIChat", + dependencies: [ + "DesignResourcesKit", + ], resources: [ .process("Resources/Assets.xcassets") ] diff --git a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift index 31d394969c..2d4bddede5 100644 --- a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift +++ b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift @@ -16,6 +16,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + import UIKit import Combine import WebKit @@ -28,6 +29,11 @@ public protocol AIChatViewControllerDelegate: AnyObject { /// - viewController: The `AIChatViewController` instance making the request. /// - url: The `URL` that is requested to be loaded. func aiChatViewController(_ viewController: AIChatViewController, didRequestToLoad url: URL) + + /// Tells the delegate that the `AIChatViewController` has finished its task. + /// + /// - Parameter viewController: The `AIChatViewController` instance that has finished. + func aiChatViewControllerDidFinish(_ viewController: AIChatViewController) } public final class AIChatViewController: UIViewController { @@ -35,6 +41,17 @@ public final class AIChatViewController: UIViewController { private let chatModel: AIChatViewModeling private var webViewController: AIChatWebViewController? + private lazy var titleBarView: TitleBarView = { + let title = UserText.aiChatTitle + + let titleBarView = TitleBarView(title: UserText.aiChatTitle) { [weak self] in + guard let self = self else { return } + self.delegate?.aiChatViewControllerDidFinish(self) + } + return titleBarView + }() + + /// Initializes a new instance of `AIChatViewController` with the specified remote settings and web view configuration. /// /// - Parameters: @@ -62,6 +79,7 @@ extension AIChatViewController { public override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .black + setupTitleBar() } public override func viewWillAppear(_ animated: Bool) { @@ -87,6 +105,18 @@ extension AIChatViewController { // MARK: - Views Setup extension AIChatViewController { + private func setupTitleBar() { + view.addSubview(titleBarView) + titleBarView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + titleBarView.heightAnchor.constraint(equalToConstant: 68) + ]) + } + private func addWebViewController() { guard webViewController == nil else { return } @@ -99,7 +129,7 @@ extension AIChatViewController { viewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - viewController.view.topAnchor.constraint(equalTo: view.topAnchor), + viewController.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor), viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) diff --git a/LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift b/LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift new file mode 100644 index 0000000000..16f8ad4b5a --- /dev/null +++ b/LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift @@ -0,0 +1,90 @@ +// +// TitleBarView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import UIKit +import DesignResourcesKit + +final class TitleBarView: UIView { + private let titleLabel: UILabel + private let closeButton: UIButton + private let handleBar: UIView + private var closeAction: (() -> Void)? + + init(title: String, closeAction: @escaping () -> Void) { + titleLabel = UILabel() + closeButton = UIButton(type: .system) + handleBar = UIView() + + self.closeAction = closeAction + + super.init(frame: .zero) + + setupView(title: title) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView(title: String) { + backgroundColor = .webViewBackgroundColor + + handleBar.backgroundColor = UIColor(white: 0.5, alpha: 0.5) + handleBar.layer.cornerRadius = Constants.handlebarHeight / 2 + handleBar.translatesAutoresizingMaskIntoConstraints = false + + titleLabel.text = title + titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + titleLabel.textColor = UIColor(designSystemColor: .textPrimary) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + closeButton.setImage(UIImage(named: "Close-24"), for: .normal) + closeButton.tintColor = UIColor(designSystemColor: .icons) + closeButton.translatesAutoresizingMaskIntoConstraints = false + closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + + addSubview(handleBar) + addSubview(titleLabel) + addSubview(closeButton) + + NSLayoutConstraint.activate([ + handleBar.topAnchor.constraint(equalTo: topAnchor, constant: 8), + handleBar.centerXAnchor.constraint(equalTo: centerXAnchor), + handleBar.widthAnchor.constraint(equalToConstant: Constants.handlebarWidth), + handleBar.heightAnchor.constraint(equalToConstant: Constants.handlebarHeight), + + titleLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + titleLabel.centerYAnchor.constraint(equalTo: handleBar.bottomAnchor, constant: 30), + + closeButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -8), + closeButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + closeButton.widthAnchor.constraint(equalToConstant: Constants.closeButtonSize), + closeButton.heightAnchor.constraint(equalToConstant: Constants.closeButtonSize) + ]) + } + + @objc private func closeButtonTapped() { + closeAction?() + } +} + +private enum Constants { + static let closeButtonSize: CGFloat = 44 + static let handlebarHeight: CGFloat = 3 + static let handlebarWidth: CGFloat = 42 +} diff --git a/LocalPackages/AIChat/Sources/AIChat/UserText.swift b/LocalPackages/AIChat/Sources/AIChat/UserText.swift index d33e745d2d..e88c31793c 100644 --- a/LocalPackages/AIChat/Sources/AIChat/UserText.swift +++ b/LocalPackages/AIChat/Sources/AIChat/UserText.swift @@ -20,5 +20,5 @@ import Foundation public struct UserText { - public static let aiChatTitle = NSLocalizedString("aichat.title", value: "DuckDuckGo AI Chat", comment: "Title for DuckDuckGo AI Chat. Should not be translated") + public static let aiChatTitle = NSLocalizedString("aichat.title", value: "Duck.ai", comment: "Title for DuckDuckGo AI Chat. Should not be translated") } From 6b6a223444ccca210e2546afd6949409387717a4 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 20 Dec 2024 11:55:24 -0300 Subject: [PATCH 22/24] Prefill AI Chat with search query (#3750) Task/Issue URL: https://app.asana.com/0/1204167627774280/1208991512395320/f **Description**: When opening AI Chat from the address bar, pre-fill it with the SERP query --- .github/workflows/adhoc.yml | 2 +- .../xcschemes/DuckDuckGo.xcscheme | 10 ++ DuckDuckGo/MainViewController.swift | 9 +- LocalPackages/AIChat/Package.swift | 4 + .../AIChat/AIChatWebViewController.swift | 7 +- .../Public API/AIChatViewController.swift | 11 ++ .../AIChat/Sources/AIChat/URL+Extension.swift | 54 +++++++ .../AIChat/Tests/URLExtensionTests.swift | 149 ++++++++++++++++++ 8 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 LocalPackages/AIChat/Sources/AIChat/URL+Extension.swift create mode 100644 LocalPackages/AIChat/Tests/URLExtensionTests.swift diff --git a/.github/workflows/adhoc.yml b/.github/workflows/adhoc.yml index 5fededcc1a..ded6413c6c 100644 --- a/.github/workflows/adhoc.yml +++ b/.github/workflows/adhoc.yml @@ -21,7 +21,7 @@ on: jobs: make-adhoc: - runs-on: macos-14-xlarge + runs-on: macos-15 name: Make ad-hoc build steps: diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index 9a439f9d03..7658de6ab4 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -110,6 +110,16 @@ ReferencedContainer = "container:DuckDuckGo.xcodeproj"> + + + + WKNavigationActionPolicy { if let url = navigationAction.request.url { - if url == chatModel.aiChatURL || navigationAction.targetFrame?.isMainFrame == false { + if url.isDuckAIURL || navigationAction.targetFrame?.isMainFrame == false { return .allow } else { delegate?.aiChatWebViewController(self, didRequestToLoad: url) diff --git a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift index 2d4bddede5..9a55405b25 100644 --- a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift +++ b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift @@ -102,6 +102,17 @@ extension AIChatViewController { } } +// MARK: - Public functions +extension AIChatViewController { + public func loadQuery(_ query: URLQueryItem) { + // Ensure the webViewController is added before loading the query + if webViewController == nil { + addWebViewController() + } + webViewController?.loadQuery(query) + } +} + // MARK: - Views Setup extension AIChatViewController { diff --git a/LocalPackages/AIChat/Sources/AIChat/URL+Extension.swift b/LocalPackages/AIChat/Sources/AIChat/URL+Extension.swift new file mode 100644 index 0000000000..b09269d7bb --- /dev/null +++ b/LocalPackages/AIChat/Sources/AIChat/URL+Extension.swift @@ -0,0 +1,54 @@ +// +// URL+Extension.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 + +extension URL { + enum Constants { + static let duckDuckGoHost = "duckduckgo.com" + static let chatQueryName = "ia" + static let chatQueryValue = "chat" + } + + func addingOrReplacingQueryItem(_ queryItem: URLQueryItem) -> URL { + guard var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + + var queryItems = urlComponents.queryItems ?? [] + queryItems.removeAll { $0.name == queryItem.name } + queryItems.append(queryItem) + + urlComponents.queryItems = queryItems + return urlComponents.url ?? self + } + + var isDuckAIURL: Bool { + guard let host = self.host, host == Constants.duckDuckGoHost else { + return false + } + + guard let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false), + let queryItems = urlComponents.queryItems else { + return false + } + + return queryItems.contains { $0.name == Constants.chatQueryName && $0.value == Constants.chatQueryValue } + } +} diff --git a/LocalPackages/AIChat/Tests/URLExtensionTests.swift b/LocalPackages/AIChat/Tests/URLExtensionTests.swift new file mode 100644 index 0000000000..caa5b50236 --- /dev/null +++ b/LocalPackages/AIChat/Tests/URLExtensionTests.swift @@ -0,0 +1,149 @@ +// +// URLExtensionTests.swift +// DuckDuckGo +// +// Copyright © 2022 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 AIChat + +final class URLExtensionTests: XCTestCase { + private enum TestURLs { + static let exampleDomain = "https://example.com" + static let duckDuckGoDomain = "https://duckduckgo.com" + + static let example = "\(exampleDomain)" + static let exampleWithKeyOldValue = "\(exampleDomain)?key=oldValue" + static let exampleWithExistingQuery = "\(exampleDomain)?existingKey=existingValue" + static let exampleWithMultipleQueryItems = "\(exampleDomain)?key1=value1&key2=value2" + static let duckDuckGoChat = "\(duckDuckGoDomain)/?ia=chat" + static let duckDuckGoWithMissingQuery = "\(duckDuckGoDomain)/" + static let duckDuckGoDifferentQuery = "\(duckDuckGoDomain)/?ia=search" + static let duckDuckGoAdditionalQueryItems = "\(duckDuckGoDomain)/?ia=chat&other=param" + } + + func testAddingQueryItemToEmptyURL() { + let url = URL(string: TestURLs.example)! + let queryItem = URLQueryItem(name: "key", value: "value") + let result = url.addingOrReplacingQueryItem(queryItem) + + XCTAssertEqual(result.scheme, "https") + XCTAssertEqual(result.host, "example.com") + XCTAssertEqual(result.queryItemsDictionary, ["key": "value"]) + } + + func testReplacingExistingQueryItem() { + let url = URL(string: TestURLs.exampleWithKeyOldValue)! + let queryItem = URLQueryItem(name: "key", value: "newValue") + let result = url.addingOrReplacingQueryItem(queryItem) + + XCTAssertEqual(result.scheme, "https") + XCTAssertEqual(result.host, "example.com") + XCTAssertEqual(result.queryItemsDictionary, ["key": "newValue"]) + } + + func testAddingQueryItemToExistingQuery() { + let url = URL(string: TestURLs.exampleWithExistingQuery)! + let queryItem = URLQueryItem(name: "newKey", value: "newValue") + let result = url.addingOrReplacingQueryItem(queryItem) + + XCTAssertEqual(result.scheme, "https") + XCTAssertEqual(result.host, "example.com") + XCTAssertEqual(result.queryItemsDictionary, ["existingKey": "existingValue", "newKey": "newValue"]) + } + + func testReplacingOneOfMultipleQueryItems() { + let url = URL(string: TestURLs.exampleWithMultipleQueryItems)! + let queryItem = URLQueryItem(name: "key1", value: "newValue1") + let result = url.addingOrReplacingQueryItem(queryItem) + + XCTAssertEqual(result.scheme, "https") + XCTAssertEqual(result.host, "example.com") + XCTAssertEqual(result.queryItemsDictionary, ["key1": "newValue1", "key2": "value2"]) + } + + func testAddingQueryItemWithNilValue() { + let url = URL(string: TestURLs.example)! + let queryItem = URLQueryItem(name: "key", value: nil) + let result = url.addingOrReplacingQueryItem(queryItem) + + XCTAssertEqual(result.scheme, "https") + XCTAssertEqual(result.host, "example.com") + XCTAssertEqual(result.queryItemsDictionary, ["key": ""]) + } + + func testReplacingQueryItemWithNilValue() { + let url = URL(string: "\(TestURLs.example)?key=value")! + let queryItem = URLQueryItem(name: "key", value: nil) + let result = url.addingOrReplacingQueryItem(queryItem) + + XCTAssertEqual(result.scheme, "https") + XCTAssertEqual(result.host, "example.com") + XCTAssertEqual(result.queryItemsDictionary, ["key": ""]) + } + + func testIsDuckAIURLWithValidURL() { + if let url = URL(string: TestURLs.duckDuckGoChat) { + XCTAssertTrue(url.isDuckAIURL, "The URL should be identified as a DuckDuckGo AI URL.") + } else { + XCTFail("Failed to create URL from string.") + } + } + + func testIsDuckAIURLWithInvalidDomain() { + if let url = URL(string: TestURLs.exampleWithExistingQuery) { + XCTAssertFalse(url.isDuckAIURL, "The URL should not be identified as a DuckDuckGo AI URL due to the domain.") + } else { + XCTFail("Failed to create URL from string.") + } + } + + func testIsDuckAIURLWithMissingQueryItem() { + if let url = URL(string: TestURLs.duckDuckGoWithMissingQuery) { + XCTAssertFalse(url.isDuckAIURL, "The URL should not be identified as a DuckDuckGo AI URL due to missing query item.") + } else { + XCTFail("Failed to create URL from string.") + } + } + + func testIsDuckAIURLWithDifferentQueryItem() { + if let url = URL(string: TestURLs.duckDuckGoDifferentQuery) { + XCTAssertFalse(url.isDuckAIURL, "The URL should not be identified as a DuckDuckGo AI URL due to different query item value.") + } else { + XCTFail("Failed to create URL from string.") + } + } + + func testIsDuckAIURLWithAdditionalQueryItems() { + if let url = URL(string: TestURLs.duckDuckGoAdditionalQueryItems) { + XCTAssertTrue(url.isDuckAIURL, "The URL should be identified as a DuckDuckGo AI URL even with additional query items.") + } else { + XCTFail("Failed to create URL from string.") + } + } +} + +extension URL { + var queryItemsDictionary: [String: String] { + var dict = [String: String]() + if let queryItems = URLComponents(url: self, resolvingAgainstBaseURL: false)?.queryItems { + for item in queryItems { + dict[item.name] = item.value ?? "" + } + } + return dict + } +} From eb5d5394080f0c7e19974d2eac630ff10506283a Mon Sep 17 00:00:00 2001 From: Lorenzo Mattei Date: Fri, 20 Dec 2024 16:54:58 +0100 Subject: [PATCH 23/24] Update to macOS 15 on GHA runners (#3753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414709148257752/1208999496624718/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. 2. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- .github/workflows/adhoc.yml | 2 +- .github/workflows/alpha.yml | 2 +- .github/workflows/end-to-end.yml | 4 ++-- .github/workflows/nightly.yml | 4 ++-- .github/workflows/promote_testflight.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/sync-end-to-end.yml | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/adhoc.yml b/.github/workflows/adhoc.yml index ded6413c6c..84be30b111 100644 --- a/.github/workflows/adhoc.yml +++ b/.github/workflows/adhoc.yml @@ -21,7 +21,7 @@ on: jobs: make-adhoc: - runs-on: macos-15 + runs-on: macos-15-xlarge name: Make ad-hoc build steps: diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index ff6cda27e6..619dc58114 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -31,7 +31,7 @@ on: jobs: make-alpha: - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge name: Make TestFlight Alpha Build timeout-minutes: 30 diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index 05b27809a3..61ae9d72c3 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -8,7 +8,7 @@ on: jobs: build-end-to-end-tests: name: Build End to End Tests - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge timeout-minutes: 30 steps: @@ -68,7 +68,7 @@ jobs: end-to-end-tests: name: End to end Tests needs: build-end-to-end-tests - runs-on: macos-14 + runs-on: macos-15 timeout-minutes: 90 strategy: matrix: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2bff554068..c7f6af3185 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7,7 +7,7 @@ on: jobs: atb-ui-tests: name: ATB UI Tests - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge timeout-minutes: 30 steps: @@ -68,7 +68,7 @@ jobs: fingerprinting-ui-tests: name: Fingerprinting UI Tests - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge timeout-minutes: 30 steps: diff --git a/.github/workflows/promote_testflight.yml b/.github/workflows/promote_testflight.yml index 641c945ad7..a9d94321c9 100644 --- a/.github/workflows/promote_testflight.yml +++ b/.github/workflows/promote_testflight.yml @@ -5,7 +5,7 @@ on: jobs: promote-testflight-to-appstore: - runs-on: macos-14 + runs-on: macos-15 steps: - name: Check out the code diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50a4fefb6f..2dffb0be3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ on: jobs: make-release: if: github.event.action == 0 || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'Merge triggers release')) # empty string returns 0; for case when workflow is triggered manually - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge name: Make App Store Connect Release steps: diff --git a/.github/workflows/sync-end-to-end.yml b/.github/workflows/sync-end-to-end.yml index b6ea062cab..adee7a00c1 100644 --- a/.github/workflows/sync-end-to-end.yml +++ b/.github/workflows/sync-end-to-end.yml @@ -8,7 +8,7 @@ on: jobs: build-for-sync-end-to-end-tests: name: Build for Sync End To End Tests - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge timeout-minutes: 30 steps: @@ -68,7 +68,7 @@ jobs: sync-end-to-end-tests: name: Sync End To End Tests needs: build-for-sync-end-to-end-tests - runs-on: macos-14 + runs-on: macos-15 timeout-minutes: 90 strategy: matrix: From b5dac568fb124f3c36480abd8ab16edc4b57c8e1 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Sat, 21 Dec 2024 05:27:20 +1100 Subject: [PATCH 24/24] Update autoconsent to v12.4.0 (#3752) Task/Issue URL: https://app.asana.com/0/1209022585506302/1209022585506302 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v12.4.0 ## Description Updates Autoconsent to version [v12.4.0](https://github.com/duckduckgo/autoconsent/releases/tag/v12.4.0). --- DuckDuckGo/Autoconsent/autoconsent-bundle.js | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index 1f3db588b9..197f6b2820 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -442,4 +442,4 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. *) - */const ve=new class{constructor(e,t=null,o=null){if(this.id=a(),this.rules=[],this.foundCmp=null,this.state={cosmeticFiltersOn:!1,filterListReported:!1,lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],heuristicPatterns:[],heuristicSnippets:[],selfTest:null},r.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new C(this)}initialize(e,t){const o=g(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),e.enableFilterList){try{0}catch(e){console.error("Error parsing filter list",e)}"loading"===document.readyState?window.addEventListener("DOMContentLoaded",(()=>{this.applyCosmeticFilters()})):this.applyCosmeticFilters()}if(this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){w.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){e.consentomatic&&Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent&&e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new u(e,this))}addConsentomaticCMP(e,t){this.rules.push(new m(`com_${e}`,t))}start(){!function(e,t=500){globalThis.requestIdleCallback?requestIdleCallback(e,{timeout:t}):setTimeout(e,0)}((()=>this._start()))}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.filterListFallback();this.updateState({lifecycle:"cmpDetected"});const o=[],i=[];for(const e of t)e.isCosmetic?i.push(e):o.push(e);let c=!1,n=await this.detectPopups(o,(async e=>{c=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(i,(async e=>{c=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return c}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return this.detectHeuristics(),0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}detectHeuristics(){if(this.config.enableHeuristicDetection){const{patterns:e,snippets:t}=function(){const e=document.documentElement.innerText,t=[],o=[];for(const i of ge){const c=e.match(i);c&&(t.push(i.toString()),o.push(...c.map((e=>e.substring(0,200)))))}return{patterns:t,snippets:o}}();e.length>0&&(e.length!==this.state.heuristicPatterns.length||this.state.heuristicPatterns.some(((t,o)=>t!==e[o])))&&(this.config.logs.lifecycle&&console.log("Heuristic patterns found",e,t),this.updateState({heuristicPatterns:e,heuristicSnippets:t}))}}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{this.detectHeuristics(),t(e)})).catch((()=>null));const i=await Promise.allSettled(o),c=[];for(const e of i)"fulfilled"===e.status&&c.push(e.value);return c}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.state.cosmeticFiltersOn&&this.undoCosmetics(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const i=this.config.logs;i.lifecycle&&console.log("checking if popup is open...",e.name);const c=await e.detectPopup().catch((t=>(i.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!c&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(i.lifecycle&&console.log(e.name,"popup is "+(c?"open":"not open")),c)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}async applyCosmeticFilters(e){if(!this.filtersEngine)return!1;const t=this.config?.logs;setTimeout((()=>{if(this.state.cosmeticFiltersOn&&!this.state.filterListReported){const o=this.domActions.elementVisible(function(e){if(e)return e.replace(/\s*{[^\\}]*}\s*/g,",").replace(/,$/,"");return""}(e),"any");o?(t?.lifecycle&&console.log("Prehide cosmetic filters matched",location.href),this.reportFilterlist()):t?.lifecycle&&console.log("Prehide cosmetic filters didn't match",location.href)}}),2e3),this.updateState({cosmeticFiltersOn:!0});try{this.cosmeticStyleSheet=await this.domActions.createOrUpdateStyleSheet(e,this.cosmeticStyleSheet),t?.lifecycle&&console.log("[cosmetics]",this.cosmeticStyleSheet,location.href),document.adoptedStyleSheets.push(this.cosmeticStyleSheet)}catch(e){return this.config.logs&&console.error("Error applying cosmetic filters",e),!1}return!0}undoCosmetics(){this.updateState({cosmeticFiltersOn:!1}),this.config.logs.lifecycle&&console.log("[undocosmetics]",this.cosmeticStyleSheet,location.href),this.domActions.removeStyleSheet(this.cosmeticStyleSheet)}reportFilterlist(){this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:"filterList"}),this.sendContentMessage({type:"popupFound",cmp:"filterList",url:location.href}),this.updateState({filterListReported:!0})}filterListFallback(){return this.updateState({lifecycle:"nothingDetected"}),!1}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=r.pending.get(e);o?(r.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{ve.receiveMessageCallback(e)}))}),null,Ce);window.autoconsentMessageCallback=e=>{ve.receiveMessageCallback(e)}}(); + */const ve=new class{constructor(e,t=null,o=null){if(this.id=a(),this.rules=[],this.foundCmp=null,this.state={cosmeticFiltersOn:!1,filterListReported:!1,lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],heuristicPatterns:[],heuristicSnippets:[],selfTest:null},r.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new C(this)}initialize(e,t){const o=g(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),e.enableFilterList){try{0}catch(e){console.error("Error parsing filter list",e)}"loading"===document.readyState?window.addEventListener("DOMContentLoaded",(()=>{this.applyCosmeticFilters()})):this.applyCosmeticFilters()}if(this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){w.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){e.consentomatic&&Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent&&e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new u(e,this))}addConsentomaticCMP(e,t){this.rules.push(new m(`com_${e}`,t))}start(){!function(e,t=500){globalThis.requestIdleCallback?requestIdleCallback(e,{timeout:t}):setTimeout(e,0)}((()=>this._start()))}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.filterListFallback();this.updateState({lifecycle:"cmpDetected"});const o=[],i=[];for(const e of t)e.isCosmetic?i.push(e):o.push(e);let c=!1,n=await this.detectPopups(o,(async e=>{c=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(i,(async e=>{c=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return c}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return this.detectHeuristics(),0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}detectHeuristics(){if(this.config.enableHeuristicDetection){const{patterns:e,snippets:t}=function(){const e=document.documentElement.innerText,t=[],o=[];for(const i of ge){const c=e.match(i);c&&(t.push(i.toString()),o.push(...c.map((e=>e.substring(0,200)))))}return{patterns:t,snippets:o}}();e.length>0&&(e.length!==this.state.heuristicPatterns.length||this.state.heuristicPatterns.some(((t,o)=>t!==e[o])))&&(this.config.logs.lifecycle&&console.log("Heuristic patterns found",e,t),this.updateState({heuristicPatterns:e,heuristicSnippets:t}))}}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{this.detectHeuristics(),t(e)})).catch((()=>{}));const i=await Promise.allSettled(o),c=[];for(const e of i)"fulfilled"===e.status&&c.push(e.value);return c}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.state.cosmeticFiltersOn&&this.undoCosmetics(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const i=this.config.logs;i.lifecycle&&console.log("checking if popup is open...",e.name);const c=await e.detectPopup().catch((t=>(i.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!c&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(i.lifecycle&&console.log(e.name,"popup is "+(c?"open":"not open")),c)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}async applyCosmeticFilters(e){if(!this.filtersEngine)return!1;const t=this.config?.logs;setTimeout((()=>{if(this.state.cosmeticFiltersOn&&!this.state.filterListReported){const o=this.domActions.elementVisible(function(e){if(e)return e.replace(/\s*{[^\\}]*}\s*/g,",").replace(/,$/,"");return""}(e),"any");o?(t?.lifecycle&&console.log("Prehide cosmetic filters matched",location.href),this.reportFilterlist()):t?.lifecycle&&console.log("Prehide cosmetic filters didn't match",location.href)}}),2e3),this.updateState({cosmeticFiltersOn:!0});try{this.cosmeticStyleSheet=await this.domActions.createOrUpdateStyleSheet(e,this.cosmeticStyleSheet),t?.lifecycle&&console.log("[cosmetics]",this.cosmeticStyleSheet,location.href),document.adoptedStyleSheets.push(this.cosmeticStyleSheet)}catch(e){return this.config.logs&&console.error("Error applying cosmetic filters",e),!1}return!0}undoCosmetics(){this.updateState({cosmeticFiltersOn:!1}),this.config.logs.lifecycle&&console.log("[undocosmetics]",this.cosmeticStyleSheet,location.href),this.domActions.removeStyleSheet(this.cosmeticStyleSheet)}reportFilterlist(){this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:"filterList"}),this.sendContentMessage({type:"popupFound",cmp:"filterList",url:location.href}),this.updateState({filterListReported:!0})}filterListFallback(){return this.updateState({lifecycle:"nothingDetected"}),!1}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=r.pending.get(e);o?(r.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{ve.receiveMessageCallback(e)}))}),null,Ce);window.autoconsentMessageCallback=e=>{ve.receiveMessageCallback(e)}}(); diff --git a/package-lock.json b/package-lock.json index 35881dad6d..645170a6a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "ios", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^12.3.0" + "@duckduckgo/autoconsent": "^12.4.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -121,9 +121,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-12.3.0.tgz", - "integrity": "sha512-mOW11Ve9DRKDkjFtAjXAP3Jd5E22YqI+4+6NTPNnOud2/02QyhM8l1vK//hTR6cmXXWZiFknpuv6qckuwMKivw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-12.4.0.tgz", + "integrity": "sha512-k7pNvq9IdPURoAhboAWx+xDMnIHKJ9JY74eft/aOOv1Lj5P8Bjv63ERyvttK5ugzvJvVyUR9GNp3DcQF/izlmA==", "license": "MPL-2.0", "dependencies": { "@ghostery/adblocker": "^2.0.4", diff --git a/package.json b/package.json index fa1f5cef7e..ff07b51cd2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^12.3.0" + "@duckduckgo/autoconsent": "^12.4.0" } }