From ea01cbeeac22e1cf1cb859973f3e545786897929 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 13:47:43 +0200 Subject: [PATCH 01/11] Removes VPN waitlist and beta code --- ...kProtectionCodeRedemptionCoordinator.swift | 26 +------- .../Networking/NetworkProtectionClient.swift | 10 --- .../UserDefaults+showMessaging.swift | 21 ------- .../Matchers/UserAttributeMatcher.swift | 9 --- .../NetworkProtectionClientTests.swift | 63 ------------------- 5 files changed, 1 insertion(+), 128 deletions(-) diff --git a/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift b/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift index 8c1d00fbf..ca5f77bce 100644 --- a/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift +++ b/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift @@ -19,15 +19,8 @@ import Foundation import Common -public protocol NetworkProtectionCodeRedeeming { - - /// Redeems an invite code with the Network Protection backend and stores the resulting auth token - func redeem(_ code: String) async throws - -} - /// Coordinates calls to the backend and oAuth token storage -public final class NetworkProtectionCodeRedemptionCoordinator: NetworkProtectionCodeRedeeming { +public final class NetworkProtectionCodeRedemptionCoordinator { private let networkClient: NetworkProtectionClient private let tokenStore: NetworkProtectionTokenStore private let isManualCodeRedemptionFlow: Bool @@ -54,21 +47,4 @@ public final class NetworkProtectionCodeRedemptionCoordinator: NetworkProtection self.errorEvents = errorEvents } - public func redeem(_ code: String) async throws { - let result = await networkClient.redeem(inviteCode: code) - switch result { - case .success(let token): - try tokenStore.store(token) - - case .failure(let error): - if case .invalidInviteCode = error, isManualCodeRedemptionFlow { - // Deliberately ignore cases where invalid invite codes are entered into the redemption form - throw error - } else { - errorEvents.fire(error.networkProtectionError) - throw error - } - } - } - } diff --git a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift index 4531f29a7..b0fc208fa 100644 --- a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift +++ b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift @@ -19,7 +19,6 @@ import Foundation protocol NetworkProtectionClient { - func redeem(inviteCode: String) async -> Result func getLocations(authToken: String) async -> Result<[NetworkProtectionLocation], NetworkProtectionClientError> func getServers(authToken: String) async -> Result<[NetworkProtectionServer], NetworkProtectionClientError> func register(authToken: String, @@ -152,10 +151,6 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient { endpointURL.appending("/register") } - var redeemURL: URL { - endpointURL.appending("/redeem") - } - private let decoder: JSONDecoder = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate, .withFullTime, .withFractionalSeconds] @@ -344,11 +339,6 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient { } } - public func redeem(inviteCode: String) async -> Result { - let requestBody = RedeemInviteCodeRequestBody(code: inviteCode) - return await retrieveAuthToken(requestBody: requestBody, endpoint: redeemURL) - } - private func retrieveAuthToken( requestBody: RequestBody, endpoint: URL diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift index d905ccde2..86efb8930 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift @@ -85,24 +85,3 @@ extension UserDefaults { public extension Notification.Name { static let vpnEntitlementMessagingDidChange = Notification.Name("com.duckduckgo.network-protection.entitlement-messaging-changed") } - -extension UserDefaults { - private var vpnEarlyAccessOverAlertAlreadyShownKey: String { - "vpnEarlyAccessOverAlertAlreadyShown" - } - - @objc - public dynamic var vpnEarlyAccessOverAlertAlreadyShown: Bool { - get { - value(forKey: vpnEarlyAccessOverAlertAlreadyShownKey) as? Bool ?? false - } - - set { - set(newValue, forKey: vpnEarlyAccessOverAlertAlreadyShownKey) - } - } - - public func resetThankYouMessaging() { - removeObject(forKey: vpnEarlyAccessOverAlertAlreadyShownKey) - } -} diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index a4043560f..13dd2890c 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -29,7 +29,6 @@ public struct UserAttributeMatcher: AttributeMatcher { private let bookmarksCount: Int private let favoritesCount: Int private let isWidgetInstalled: Bool - private let isNetPWaitlistUser: Bool private let daysSinceNetPEnabled: Int public init(statisticsStore: StatisticsStore, @@ -39,7 +38,6 @@ public struct UserAttributeMatcher: AttributeMatcher { favoritesCount: Int, appTheme: String, isWidgetInstalled: Bool, - isNetPWaitlistUser: Bool, daysSinceNetPEnabled: Int ) { self.statisticsStore = statisticsStore @@ -49,7 +47,6 @@ public struct UserAttributeMatcher: AttributeMatcher { self.bookmarksCount = bookmarksCount self.favoritesCount = favoritesCount self.isWidgetInstalled = isWidgetInstalled - self.isNetPWaitlistUser = isNetPWaitlistUser self.daysSinceNetPEnabled = daysSinceNetPEnabled } @@ -97,12 +94,6 @@ public struct UserAttributeMatcher: AttributeMatcher { } return BooleanMatchingAttribute(value).matches(value: isWidgetInstalled) - case let matchingAttribute as IsNetPWaitlistUserMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: isNetPWaitlistUser) case let matchingAttribute as DaysSinceNetPEnabledMatchingAttribute: if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { return IntMatchingAttribute(matchingAttribute.value).matches(value: daysSinceNetPEnabled) diff --git a/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift b/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift index 6fe82606c..469e33e2d 100644 --- a/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift +++ b/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift @@ -79,69 +79,6 @@ final class NetworkProtectionClientTests: XCTestCase { } } - // MARK: redeem(inviteCode:) - - func testRedeemSuccess() async { - let token = "a6s7ad6ad76aasa7s6a" - let successData = redeemSuccessData(token: token) - MockURLProtocol.stubs[client.redeemURL] = (response: HTTPURLResponse(url: client.redeemURL, statusCode: 200)!, - .success(successData)) - - let result = await client.redeem(inviteCode: "DH76F8S") - - XCTAssertEqual(try? result.get(), token) - } - - func testRedeem400Response() async { - let emptyData = "".data(using: .utf8)! - MockURLProtocol.stubs[client.redeemURL] = (response: HTTPURLResponse(url: client.redeemURL, statusCode: 400)!, - .success(emptyData)) - - let result = await client.redeem(inviteCode: "DH76F8S") - - guard case .failure(let error) = result, case .invalidInviteCode = error else { - XCTFail("Expected an invalidInviteCode error to be thrown") - return - } - } - - func testRedeemNon200Or400Response() async { - let emptyData = "".data(using: .utf8)! - - for code in [401, 304, 500] { - MockURLProtocol.stubs[client.redeemURL] = (response: HTTPURLResponse(url: client.redeemURL, statusCode: code)!, - .success(emptyData)) - - let result = await client.redeem(inviteCode: "DH76F8S") - - guard case .failure(let error) = result, case .failedToRedeemInviteCode = error else { - XCTFail("Expected a failedToRedeemInviteCode error to be thrown") - return - } - } - } - - func testRedeemDecodeFailure() async { - let undecodableData = "sdfghj".data(using: .utf8)! - MockURLProtocol.stubs[client.redeemURL] = (response: HTTPURLResponse(url: client.redeemURL, statusCode: 200)!, - .success(undecodableData)) - - let result = await client.redeem(inviteCode: "DH76F8S") - - guard case .failure(let error) = result, case .failedToParseRedeemResponse = error else { - XCTFail("Expected a failedToRedeemInviteCode error to be thrown") - return - } - } - - private func redeemSuccessData(token: String) -> Data { - return """ - { - "token": "\(token)" - } - """.data(using: .utf8)! - } - // MARK: locations(authToken:) func testLocationsSuccess() async { From 3965d2cdf3725fc3718c9e8e1ebc17c778faead3 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 14:05:18 +0200 Subject: [PATCH 02/11] Removes waitlist info --- .../JsonToRemoteMessageModelMapper.swift | 2 -- .../Model/MatchingAttributes.swift | 25 ------------------- .../JsonToRemoteConfigModelMapperTests.swift | 4 --- .../Matchers/UserAttributeMatcherTests.swift | 10 -------- .../RemoteMessagingConfigMatcherTests.swift | 4 --- .../RemoteMessagingConfigProcessorTests.swift | 2 -- .../Resources/remote-messaging-config.json | 3 --- 7 files changed, 50 deletions(-) diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index 5a119c7c2..13a9dd8e3 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -36,7 +36,6 @@ private enum AttributesKey: String, CaseIterable { case favorites case appTheme case daysSinceInstalled - case isNetPWaitlistUser case daysSinceNetPEnabled func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { @@ -56,7 +55,6 @@ private enum AttributesKey: String, CaseIterable { case .favorites: return FavoritesMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .appTheme: return AppThemeMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .daysSinceInstalled: return DaysSinceInstalledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) - case .isNetPWaitlistUser: return IsNetPWaitlistUserMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .daysSinceNetPEnabled: return DaysSinceNetPEnabledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 7f6551c18..c1906a31e 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -608,31 +608,6 @@ struct RangeStringNumericMatchingAttribute: Equatable { } } -struct IsNetPWaitlistUserMatchingAttribute: MatchingAttribute, Equatable { - var value: Bool? - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Bool { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: Bool?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: IsNetPWaitlistUserMatchingAttribute, rhs: IsNetPWaitlistUserMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - struct DaysSinceNetPEnabledMatchingAttribute: MatchingAttribute, Equatable { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift index 6b0a038c3..a4979bc34 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift @@ -131,10 +131,6 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { attribs = rule8?.value.filter { $0 is DaysSinceNetPEnabledMatchingAttribute } XCTAssertEqual(attribs?.count, 1) XCTAssertEqual(attribs?.first as? DaysSinceNetPEnabledMatchingAttribute, DaysSinceNetPEnabledMatchingAttribute(min: 5, fallback: nil)) - - attribs = rule8?.value.filter { $0 is IsNetPWaitlistUserMatchingAttribute } - XCTAssertEqual(attribs?.count, 1) - XCTAssertEqual(attribs?.first as? IsNetPWaitlistUserMatchingAttribute, IsNetPWaitlistUserMatchingAttribute(value: true, fallback: nil)) } func testWhenJsonMessagesHaveUnknownTypesThenMessagesNotMappedIntoConfig() throws { diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift index d8b63f122..968545ae3 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift @@ -207,16 +207,6 @@ class UserAttributeMatcherTests: XCTestCase { // MARK: - Network Protection Waitlist - func testWhenIsNetPWaitlistUserMatchesThenReturnMatch() throws { - XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsNetPWaitlistUserMatchingAttribute(value: true, fallback: nil)), - .match) - } - - func testWhenIsNetPWaitlistUserDoesNotMatchThenReturnFail() throws { - XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsNetPWaitlistUserMatchingAttribute(value: false, fallback: nil)), - .fail) - } - func testWhenDaysSinceNetPEnabledMatchesThenReturnMatch() throws { XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: DaysSinceNetPEnabledMatchingAttribute(min: 1, fallback: nil)), .match) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift index d56932647..9ab9d49a2 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift @@ -43,7 +43,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), dismissedMessageIds: [] ) @@ -113,7 +112,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), dismissedMessageIds: []) @@ -182,7 +180,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), dismissedMessageIds: ["1"]) @@ -212,7 +209,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), dismissedMessageIds: []) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift index 24d378591..e14f868b3 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift @@ -36,7 +36,6 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), dismissedMessageIds: [] ) @@ -64,7 +63,6 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), dismissedMessageIds: []) diff --git a/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json b/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json index fea7adc2d..9005218c2 100644 --- a/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json +++ b/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json @@ -237,9 +237,6 @@ { "id": 8, "attributes": { - "isNetPWaitlistUser": { - "value": true - }, "daysSinceNetPEnabled": { "min": 5 } From 7bd1df430c520c0ec50febcc61cdb5d9d05c5630 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 14:18:54 +0200 Subject: [PATCH 03/11] Fixes some swiftlint annoyance --- .../NetworkProtection/ExtensionMessage/ExtensionMessage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift index dd00d0aa3..f1ed48dab 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift @@ -70,7 +70,7 @@ public enum ExtensionMessage: RawRepresentable { case simulateConnectionInterruption case getDataVolume - // swiftlint:disable:next cyclomatic_complexity function_body_length + // swiftlint:disable:next cyclomatic_complexity function_body_length superfluous_disable_command public init?(rawValue data: Data) { let name = data.first.flatMap(Name.init(rawValue:)) switch name { From cc289ac81b8bee570c0e2ca3320a2e15ed25b710 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 14:21:18 +0200 Subject: [PATCH 04/11] Trying to fix swiftlint --- .../NetworkProtection/ExtensionMessage/ExtensionMessage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift index f1ed48dab..6461945a9 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift @@ -70,7 +70,7 @@ public enum ExtensionMessage: RawRepresentable { case simulateConnectionInterruption case getDataVolume - // swiftlint:disable:next cyclomatic_complexity function_body_length superfluous_disable_command + // swiftlint:disable:next cyclomatic_complexity public init?(rawValue data: Data) { let name = data.first.flatMap(Name.init(rawValue:)) switch name { From 13753f7db55a84bf5cf029047c72daee320adead Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 14:32:35 +0200 Subject: [PATCH 05/11] Finally fixes swiftlint crazyness --- .../NetworkProtection/ExtensionMessage/ExtensionMessage.swift | 2 +- Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift index 6461945a9..dd00d0aa3 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift @@ -70,7 +70,7 @@ public enum ExtensionMessage: RawRepresentable { case simulateConnectionInterruption case getDataVolume - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length public init?(rawValue data: Data) { let name = data.first.flatMap(Name.init(rawValue:)) switch name { diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 13dd2890c..ac57d3532 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -50,7 +50,7 @@ public struct UserAttributeMatcher: AttributeMatcher { self.daysSinceNetPEnabled = daysSinceNetPEnabled } - // swiftlint:disable:next cyclomatic_complexity function_body_length + // swiftlint:disable:next cyclomatic_complexity func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as AppThemeMatchingAttribute: From d3f0db7088befd23bcb09d845c0b355a6193f4fb Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 14 May 2024 11:47:38 +0200 Subject: [PATCH 06/11] Fixes some unit tests and errors --- .../Matchers/UserAttributeMatcherTests.swift | 1 - .../NetworkProtectionClientTests.swift | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift index 968545ae3..80881eb72 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift @@ -53,7 +53,6 @@ class UserAttributeMatcherTests: XCTestCase { favoritesCount: 88, appTheme: "default", isWidgetInstalled: true, - isNetPWaitlistUser: true, daysSinceNetPEnabled: 3) } diff --git a/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift b/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift index 75e5abe1a..6b1011273 100644 --- a/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift +++ b/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift @@ -52,7 +52,7 @@ final class NetworkProtectionClientTests: XCTestCase { func testRegister401Response_ThrowsInvalidTokenError() async { let emptyData = "".data(using: .utf8)! - MockURLProtocol.stubs[client.redeemURL] = (response: HTTPURLResponse(url: client.registerKeyURL, statusCode: 401)!, + MockURLProtocol.stubs[client.registerKeyURL] = (response: HTTPURLResponse(url: client.registerKeyURL, statusCode: 401)!, .success(emptyData)) let body = RegisterKeyRequestBody(publicKey: .testData, serverSelection: .server(name: "MockServer")) @@ -68,7 +68,7 @@ final class NetworkProtectionClientTests: XCTestCase { func testGetServer401Response_ThrowsInvalidTokenError() async { let emptyData = "".data(using: .utf8)! - MockURLProtocol.stubs[client.redeemURL] = (response: HTTPURLResponse(url: client.serversURL, statusCode: 401)!, + MockURLProtocol.stubs[client.serversURL] = (response: HTTPURLResponse(url: client.serversURL, statusCode: 401)!, .success(emptyData)) let result = await client.getServers(authToken: "anAuthToken") @@ -83,7 +83,7 @@ final class NetworkProtectionClientTests: XCTestCase { func testLocationsSuccess() async { let successData = TestData.mockLocations - MockURLProtocol.stubs[client.locationsURL] = (response: HTTPURLResponse(url: client.redeemURL, statusCode: 200)!, + MockURLProtocol.stubs[client.locationsURL] = (response: HTTPURLResponse(url: client.locationsURL, statusCode: 200)!, .success(successData)) let result = await client.getLocations(authToken: "DH76F8S") @@ -122,7 +122,7 @@ final class NetworkProtectionClientTests: XCTestCase { func testLocationsDecodeFailure() async { let undecodableData = "sdfghj".data(using: .utf8)! - MockURLProtocol.stubs[client.locationsURL] = (response: HTTPURLResponse(url: client.redeemURL, statusCode: 200)!, + MockURLProtocol.stubs[client.locationsURL] = (response: HTTPURLResponse(url: client.locationsURL, statusCode: 200)!, .success(undecodableData)) let result = await client.getLocations(authToken: "DH76F8S") From 6821ee51e297d475283ddefa454c81a04c9dad4f Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 14 May 2024 12:21:37 +0200 Subject: [PATCH 07/11] Fixes a BSK test --- .../Mappers/JsonToRemoteConfigModelMapperTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift index a4979bc34..2dbc78f99 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift @@ -127,7 +127,7 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { let rule8 = config.rules.filter { $0.key == 8 }.first XCTAssertNotNil(rule8) - XCTAssertTrue(rule8?.value.count == 2) + XCTAssertTrue(rule8?.value.count == 1) attribs = rule8?.value.filter { $0 is DaysSinceNetPEnabledMatchingAttribute } XCTAssertEqual(attribs?.count, 1) XCTAssertEqual(attribs?.first as? DaysSinceNetPEnabledMatchingAttribute, DaysSinceNetPEnabledMatchingAttribute(min: 5, fallback: nil)) From 0463f58c6bf197de3abe2fcab13d9f4c82bd5b92 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 23 May 2024 15:50:44 -0400 Subject: [PATCH 08/11] Revert "Autofill engagement KPIs for pixel reporting (#830)" This reverts commit e1e436422bc167933baa0f90838958f2ac7119f3. --- .../Autofill/AutofillPixelReporter.swift | 243 ---------- .../AutofillUserScript+SecureVault.swift | 11 - .../AutofillDatabaseProvider.swift | 8 - .../SecureVault/AutofillSecureVault.swift | 16 +- .../SecureVault/SecureVaultManager.swift | 2 - Sources/Common/Extensions/DateExtension.swift | 5 - .../Credentials/CredentialsProvider.swift | 5 - .../Autofill/AutofillPixelReporterTests.swift | 421 ------------------ .../MockAutofillDatabaseProvider.swift | 4 - 9 files changed, 8 insertions(+), 707 deletions(-) delete mode 100644 Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift delete mode 100644 Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift diff --git a/Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift b/Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift deleted file mode 100644 index b4ed71d70..000000000 --- a/Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift +++ /dev/null @@ -1,243 +0,0 @@ -// -// AutofillPixelReporter.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Persistence -import SecureStorage -import Common - -public enum AutofillPixelEvent { - case autofillActiveUser - case autofillEnabledUser - case autofillOnboardedUser - case autofillLoginsStacked - case autofillCreditCardsStacked - - enum Parameter { - static let countBucket = "count_bucket" - } -} - -public final class AutofillPixelReporter { - - enum Keys { - static let autofillSearchDauDateKey = "com.duckduckgo.app.autofill.SearchDauDate" - static let autofillFillDateKey = "com.duckduckgo.app.autofill.FillDate" - static let autofillOnboardedUserKey = "com.duckduckgo.app.autofill.OnboardedUser" - } - - enum BucketName: String { - case none - case few - case some - case many - case lots - } - - private enum EventType { - case fill - case searchDAU - } - - private let userDefaults: UserDefaults - private let eventMapping: EventMapping - private var secureVault: (any AutofillSecureVault)? - private var reporter: SecureVaultReporting? - // Third party password manager - private let passwordManager: PasswordManager? - private var installDate: Date? - - private var autofillSearchDauDate: Date? { userDefaults.object(forKey: Keys.autofillSearchDauDateKey) as? Date ?? .distantPast } - private var autofillFillDate: Date? { userDefaults.object(forKey: Keys.autofillFillDateKey) as? Date ?? .distantPast } - private var autofillOnboardedUser: Bool { userDefaults.object(forKey: Keys.autofillOnboardedUserKey) as? Bool ?? false } - - public init(userDefaults: UserDefaults, - eventMapping: EventMapping, - secureVault: (any AutofillSecureVault)? = nil, - reporter: SecureVaultReporting? = nil, - passwordManager: PasswordManager? = nil, - installDate: Date? = nil - ) { - self.userDefaults = userDefaults - self.eventMapping = eventMapping - self.secureVault = secureVault - self.reporter = reporter - self.passwordManager = passwordManager - self.installDate = installDate - - createNotificationObservers() - } - - public func resetStoreDefaults() { - userDefaults.set(Date.distantPast, forKey: Keys.autofillSearchDauDateKey) - userDefaults.set(Date.distantPast, forKey: Keys.autofillFillDateKey) - userDefaults.set(false, forKey: Keys.autofillOnboardedUserKey) - } - - private func createNotificationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSearchDAU), name: .searchDAU, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveFillEvent), name: .autofillFillEvent, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil) - } - - @objc - private func didReceiveSearchDAU() { - guard let autofillSearchDauDate = autofillSearchDauDate, !Date.isSameDay(Date(), autofillSearchDauDate) else { - return - } - - userDefaults.set(Date(), forKey: Keys.autofillSearchDauDateKey) - - firePixelsFor(.searchDAU) - } - - @objc - private func didReceiveFillEvent() { - guard let autofillFillDate = autofillFillDate, !Date.isSameDay(Date(), autofillFillDate) else { - return - } - - userDefaults.set(Date(), forKey: Keys.autofillFillDateKey) - - firePixelsFor(.fill) - } - - @objc - private func didReceiveSaveEvent() { - guard !autofillOnboardedUser else { - return - } - - if shouldFireOnboardedUserPixel() { - eventMapping.fire(.autofillOnboardedUser) - } - } - - private func firePixelsFor(_ type: EventType) { - if shouldFireActiveUserPixel() { - eventMapping.fire(.autofillActiveUser) - - if let accountsCountBucket = getAccountsCountBucket() { - eventMapping.fire(.autofillLoginsStacked, parameters: [AutofillPixelEvent.Parameter.countBucket: accountsCountBucket]) - } - - if let cardsCount = try? vault()?.creditCardsCount() { - eventMapping.fire(.autofillCreditCardsStacked, parameters: [AutofillPixelEvent.Parameter.countBucket: creditCardsBucketNameFrom(count: cardsCount)]) - } - } - - switch type { - case .searchDAU: - if shouldFireEnabledUserPixel() { - eventMapping.fire(.autofillEnabledUser) - } - default: - break - } - } - - private func getAccountsCountBucket() -> String? { - if let passwordManager = passwordManager, passwordManager.isEnabled { - // if a user is using a password manager we can't get a count of their passwords so we are assuming they are likely to have a lot of passwords saved - return BucketName.lots.rawValue - } else if let accountsCount = try? vault()?.accountsCount() { - return accountsBucketNameFrom(count: accountsCount) - } - return nil - } - - private func shouldFireActiveUserPixel() -> Bool { - let today = Date() - if Date.isSameDay(today, autofillSearchDauDate) && Date.isSameDay(today, autofillFillDate) { - return true - } - return false - } - - private func shouldFireEnabledUserPixel() -> Bool { - if Date.isSameDay(Date(), autofillSearchDauDate) { - if let passwordManager = passwordManager, passwordManager.isEnabled { - return true - } else if let count = try? vault()?.accountsCount(), count >= 10 { - return true - } - } - return false - } - - private func shouldFireOnboardedUserPixel() -> Bool { - guard !autofillOnboardedUser, let installDate = installDate else { - return false - } - - let pastWeek = Date().addingTimeInterval(.days(-7)) - - if installDate >= pastWeek { - if let passwordManager = passwordManager, passwordManager.isEnabled { - return true - } else if let count = try? vault()?.accountsCount(), count > 0 { - userDefaults.set(true, forKey: Keys.autofillOnboardedUserKey) - return true - } - } else { - userDefaults.set(true, forKey: Keys.autofillOnboardedUserKey) - } - - return false - } - - private func vault() -> (any AutofillSecureVault)? { - if secureVault == nil { - secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: reporter) - } - return secureVault - } - - private func accountsBucketNameFrom(count: Int) -> String { - if count == 0 { - return BucketName.none.rawValue - } else if count < 4 { - return BucketName.few.rawValue - } else if count < 11 { - return BucketName.some.rawValue - } else if count < 50 { - return BucketName.many.rawValue - } else { - return BucketName.lots.rawValue - } - } - - private func creditCardsBucketNameFrom(count: Int) -> String { - if count == 0 { - return BucketName.none.rawValue - } else if count < 4 { - return BucketName.some.rawValue - } else { - return BucketName.many.rawValue - } - } - -} - -public extension NSNotification.Name { - - static let autofillFillEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.AutofillFillEvent") - static let autofillSaveEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.AutofillSaveEvent") - static let searchDAU: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.SearchDAU") - -} diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift index ca5170f5c..e3a00c4bf 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift @@ -721,10 +721,6 @@ extension AutofillUserScript { case autofillPrivateAddress = "autofill_private_address" } - private enum IdentityPixelName: String { - case autofillIdentity = "autofill_identity" - } - /// The pixel name sent by the JS layer. This name does not include the platform on which it was sent. private let originalPixelName: String @@ -743,13 +739,6 @@ extension AutofillUserScript { } } - public var isIdentityPixel: Bool { - if case IdentityPixelName.autofillIdentity.rawValue = originalPixelName { - return true - } - return false - } - public var pixelName: String { switch originalPixelName { case EmailPixelName.autofillPersonalAddress.rawValue: diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift b/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift index 70ec30ddc..73470a359 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift @@ -57,7 +57,6 @@ public protocol AutofillDatabaseProvider: SecureStorageDatabaseProvider { func deleteIdentityForIdentityId(_ identityId: Int64) throws func creditCards() throws -> [SecureVaultModels.CreditCard] - func creditCardsCount() throws -> Int func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? @discardableResult func storeCreditCard(_ creditCard: SecureVaultModels.CreditCard) throws -> Int64 @@ -558,13 +557,6 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro } } - public func creditCardsCount() throws -> Int { - let count = try db.read { - try SecureVaultModels.CreditCard.fetchCount($0) - } - return count - } - public func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { try db.read { return try SecureVaultModels.CreditCard.fetchOne($0, sql: """ diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift b/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift index e411c25b6..498a7a2b7 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift @@ -87,7 +87,6 @@ public protocol AutofillSecureVault: SecureVault { func deleteIdentityFor(identityId: Int64) throws func creditCards() throws -> [SecureVaultModels.CreditCard] - func creditCardsCount() throws -> Int func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard? @discardableResult @@ -232,8 +231,15 @@ public class DefaultAutofillSecureVault: AutofillSe } public func accountsCount() throws -> Int { - return try executeThrowingDatabaseOperation { + lock.lock() + defer { + lock.unlock() + } + + do { return try self.providers.database.accountsCount() + } catch { + throw SecureStorageError.databaseError(cause: error) } } @@ -520,12 +526,6 @@ public class DefaultAutofillSecureVault: AutofillSe } } - public func creditCardsCount() throws -> Int { - return try executeThrowingDatabaseOperation { - return try self.providers.database.creditCardsCount() - } - } - public func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? { return try executeThrowingDatabaseOperation { guard var card = try self.providers.database.creditCardForCardId(id) else { diff --git a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift index 8fd8f3ebd..eda3a4569 100644 --- a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift +++ b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift @@ -246,7 +246,6 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { if autofilldata.trigger == .passwordGeneration { autogeneratedPassword = data.credentials?.autogenerated ?? false - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) } // Account for cases when the user has manually changed an autogenerated password or private email @@ -617,7 +616,6 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { var account = SecureVaultModels.WebsiteAccount(username: username, domain: domain, lastUsed: Date()) let credentials = try? vault?.storeWebsiteCredentials(SecureVaultModels.WebsiteCredentials(account: account, password: password)) account.id = String(credentials ?? -1) - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) return account } diff --git a/Sources/Common/Extensions/DateExtension.swift b/Sources/Common/Extensions/DateExtension.swift index 7bcba129c..31ca8730c 100644 --- a/Sources/Common/Extensions/DateExtension.swift +++ b/Sources/Common/Extensions/DateExtension.swift @@ -41,11 +41,6 @@ public extension Date { return Calendar.current.date(byAdding: .day, value: -days, to: Date())! } - static func isSameDay(_ date1: Date, _ date2: Date?) -> Bool { - guard let date2 = date2 else { return false } - return Calendar.current.isDate(date1, inSameDayAs: date2) - } - static var startOfDayTomorrow: Date { let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())! return Calendar.current.startOfDay(for: tomorrow) diff --git a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift index 020f711cd..1e9091456 100644 --- a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift +++ b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift @@ -209,11 +209,6 @@ public final class CredentialsProvider: DataProvider { } else { lastSyncLocalTimestamp = clientTimestamp } - - if !received.isEmpty { - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) - } - syncDidFinish() } diff --git a/Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift b/Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift deleted file mode 100644 index e56a141ef..000000000 --- a/Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift +++ /dev/null @@ -1,421 +0,0 @@ -// -// AutofillPixelReporterTests.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -import TestUtils -import Common -import SecureStorage -import SecureStorageTestsUtils -@testable import BrowserServicesKit - -final class AutofillPixelReporterTests: XCTestCase { - - private class MockEventMapping: EventMapping { - static var events: [AutofillPixelEvent] = [] - static var loginsParam: String? - static var creditCardsParam: String? - - public init() { - super.init { event, _, param, _ in - Self.events.append(event) - switch event { - case .autofillLoginsStacked: - Self.loginsParam = param?[AutofillPixelEvent.Parameter.countBucket] - case .autofillCreditCardsStacked: - Self.creditCardsParam = param?[AutofillPixelEvent.Parameter.countBucket] - default: - break - } - } - } - - override init(mapping: @escaping EventMapping.Mapping) { - fatalError("Use init()") - } - } - - private var mockCryptoProvider = MockCryptoProvider() - private var mockDatabaseProvider = (try! MockAutofillDatabaseProvider()) - private var mockKeystoreProvider = MockKeystoreProvider() - private var vault: (any AutofillSecureVault)! - private var eventMapping: MockEventMapping! - private var userDefaults: UserDefaults! - private let testGroupName = "autofill-reporter" - - override func setUpWithError() throws { - try super.setUpWithError() - - userDefaults = UserDefaults(suiteName: testGroupName)! - userDefaults.removePersistentDomain(forName: testGroupName) - - let providers = SecureStorageProviders(crypto: mockCryptoProvider, - database: mockDatabaseProvider, - keystore: mockKeystoreProvider) - - vault = DefaultAutofillSecureVault(providers: providers) - - eventMapping = MockEventMapping() - MockEventMapping.events.removeAll() - } - - override func tearDownWithError() throws { - vault = nil - eventMapping = nil - userDefaults.removePersistentDomain(forName: testGroupName) - - try super.tearDownWithError() - } - - func testWhenFirstFillAndSearchDauIsNotTodayThenNoEventsAreFired() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenFirstFillAndSearchDauIsTodayAndAccountsCountIsZeroThenThreeEventsAreFiredWithNoneParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - setAutofillSearchDauDate(daysAgo: 0) - - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 3) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndFillDateIsNotTodayAndAccountsCountIsZeroThenNoEventsAreFired() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 0) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenFirstSearchDauAndFillDateIsNotTodayAndAndAccountsCountIsTenThenThenOneEventIsFired() throws { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 10) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 1) - let event = try XCTUnwrap(MockEventMapping.events.first) - XCTAssertEqual(event, .autofillEnabledUser) - } - - func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsZeroThenThreeEventsAreFiredWithNoneParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 0) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 3) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsThreeThenTwoEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 3) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 3) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.few.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsTenThenFourEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 10) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 4) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.some.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsElevenThenFourEventsAreFiredWithManyParam() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 11) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 4) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.many.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsFortyThenFourEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 40) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 4) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.many.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsFiftyThenFourEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 50) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 4) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.lots.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndCreditCardsCountIsOneThenThreeEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 0) - createCreditCardsInVault(count: 1) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 3) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.some.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndCreditCardsCountIsThreeThenThreeEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 0) - createCreditCardsInVault(count: 3) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 3) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.some.rawValue) - } - - func testWhenFirstSearchDauAndThenFirstFillAndCreditCardsCountIsFourThenThreeEventsAreFiredWithCorrectParams() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 0) - createCreditCardsInVault(count: 4) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 3) - XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) - XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) - XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) - XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.many.rawValue) - } - - func testWhenSubsequentFillAndSearchDauIsNotTodayThenNoEventsAreFired() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - setAutofillSearchDauDate(daysAgo: 1) - - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenSubsequentFillAndSearchDauIsTodayThenNoEventsAreFired() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - setAutofillSearchDauDate(daysAgo: 0) - setAutofillFillDate(daysAgo: 0) - - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenSubsequentSearchDauAndFillDateIsNotTodayThenNoEventsAreFired() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - setAutofillSearchDauDate(daysAgo: 0) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenSubsequentSearchDauAndFillDateIsTodayThenNoEventsAreFired() { - let autofillPixelReporter = createAutofillPixelReporter() - autofillPixelReporter.resetStoreDefaults() - setAutofillSearchDauDate(daysAgo: 0) - setAutofillFillDate(daysAgo: 0) - - NotificationCenter.default.post(name: .searchDAU, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenSaveAndUserIsAlreadyOnboardedThenOnboardedUserPixelShouldNotBeFired() { - let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-1))) - autofillPixelReporter.resetStoreDefaults() - userDefaults.set(true, forKey: AutofillPixelReporter.Keys.autofillOnboardedUserKey) - - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenSaveAndNotOnboardedAndInstallDateIsNilThenOnboardedUserPixelShouldNotBeFired() { - let autofillPixelReporter = createAutofillPixelReporter(installDate: nil) - autofillPixelReporter.resetStoreDefaults() - - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsLessThanSevenDaysAgoAndAccountsCountIsZeroThenOnboardedUserPixelShouldNotBeFired() { - let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-4))) - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 0) - - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsLessThanSevenDaysAgoAndAccountsCountIsGreaterThanZeroThenOnboardedUserPixelShouldBeFiredAndAutofillOnboardedUserShouldBeTrue() throws { - let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-4))) - autofillPixelReporter.resetStoreDefaults() - createAccountsInVault(count: 1) - - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 1) - let event = try XCTUnwrap(MockEventMapping.events.first) - XCTAssertEqual(event, .autofillOnboardedUser) - let onboardedState = try XCTUnwrap(getAutofillOnboardedUserState()) - XCTAssertTrue(onboardedState) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsGreaterThanSevenDaysAgoThenOnboardedUserPixelShouldNotBeFiredAndAutofillOnboardedUserShouldBeTrue() throws { - let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-8))) - autofillPixelReporter.resetStoreDefaults() - - NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) - - XCTAssertEqual(MockEventMapping.events.count, 0) - let onboardedState = try XCTUnwrap(getAutofillOnboardedUserState()) - XCTAssertTrue(onboardedState) - } - - private func createAutofillPixelReporter(installDate: Date? = Date()) -> AutofillPixelReporter { - return AutofillPixelReporter(userDefaults: userDefaults, - eventMapping: eventMapping, - secureVault: vault, - installDate: installDate) - } - - private func createAccountsInVault(count: Int) { - try? vault.deleteAllWebsiteCredentials() - - for i in 0.. Bool? { - return userDefaults.object(forKey: AutofillPixelReporter.Keys.autofillOnboardedUserKey) as? Bool - } - -} diff --git a/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift b/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift index 4c767289e..7e674a1bf 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift @@ -142,10 +142,6 @@ internal class MockAutofillDatabaseProvider: AutofillDatabaseProvider { return Array(_creditCards.values) } - func creditCardsCount() throws -> Int { - return _creditCards.count - } - func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { return _creditCards[cardId] } From bf35532460de7dec432423e8e2c7e058d585621d Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 23 May 2024 15:52:20 -0400 Subject: [PATCH 09/11] Reapply "Autofill engagement KPIs for pixel reporting (#830)" This reverts commit 0463f58c6bf197de3abe2fcab13d9f4c82bd5b92. --- .../Autofill/AutofillPixelReporter.swift | 243 ++++++++++ .../AutofillUserScript+SecureVault.swift | 11 + .../AutofillDatabaseProvider.swift | 8 + .../SecureVault/AutofillSecureVault.swift | 16 +- .../SecureVault/SecureVaultManager.swift | 2 + Sources/Common/Extensions/DateExtension.swift | 5 + .../Credentials/CredentialsProvider.swift | 5 + .../Autofill/AutofillPixelReporterTests.swift | 421 ++++++++++++++++++ .../MockAutofillDatabaseProvider.swift | 4 + 9 files changed, 707 insertions(+), 8 deletions(-) create mode 100644 Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift create mode 100644 Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift diff --git a/Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift b/Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift new file mode 100644 index 000000000..b4ed71d70 --- /dev/null +++ b/Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift @@ -0,0 +1,243 @@ +// +// AutofillPixelReporter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Persistence +import SecureStorage +import Common + +public enum AutofillPixelEvent { + case autofillActiveUser + case autofillEnabledUser + case autofillOnboardedUser + case autofillLoginsStacked + case autofillCreditCardsStacked + + enum Parameter { + static let countBucket = "count_bucket" + } +} + +public final class AutofillPixelReporter { + + enum Keys { + static let autofillSearchDauDateKey = "com.duckduckgo.app.autofill.SearchDauDate" + static let autofillFillDateKey = "com.duckduckgo.app.autofill.FillDate" + static let autofillOnboardedUserKey = "com.duckduckgo.app.autofill.OnboardedUser" + } + + enum BucketName: String { + case none + case few + case some + case many + case lots + } + + private enum EventType { + case fill + case searchDAU + } + + private let userDefaults: UserDefaults + private let eventMapping: EventMapping + private var secureVault: (any AutofillSecureVault)? + private var reporter: SecureVaultReporting? + // Third party password manager + private let passwordManager: PasswordManager? + private var installDate: Date? + + private var autofillSearchDauDate: Date? { userDefaults.object(forKey: Keys.autofillSearchDauDateKey) as? Date ?? .distantPast } + private var autofillFillDate: Date? { userDefaults.object(forKey: Keys.autofillFillDateKey) as? Date ?? .distantPast } + private var autofillOnboardedUser: Bool { userDefaults.object(forKey: Keys.autofillOnboardedUserKey) as? Bool ?? false } + + public init(userDefaults: UserDefaults, + eventMapping: EventMapping, + secureVault: (any AutofillSecureVault)? = nil, + reporter: SecureVaultReporting? = nil, + passwordManager: PasswordManager? = nil, + installDate: Date? = nil + ) { + self.userDefaults = userDefaults + self.eventMapping = eventMapping + self.secureVault = secureVault + self.reporter = reporter + self.passwordManager = passwordManager + self.installDate = installDate + + createNotificationObservers() + } + + public func resetStoreDefaults() { + userDefaults.set(Date.distantPast, forKey: Keys.autofillSearchDauDateKey) + userDefaults.set(Date.distantPast, forKey: Keys.autofillFillDateKey) + userDefaults.set(false, forKey: Keys.autofillOnboardedUserKey) + } + + private func createNotificationObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSearchDAU), name: .searchDAU, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveFillEvent), name: .autofillFillEvent, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil) + } + + @objc + private func didReceiveSearchDAU() { + guard let autofillSearchDauDate = autofillSearchDauDate, !Date.isSameDay(Date(), autofillSearchDauDate) else { + return + } + + userDefaults.set(Date(), forKey: Keys.autofillSearchDauDateKey) + + firePixelsFor(.searchDAU) + } + + @objc + private func didReceiveFillEvent() { + guard let autofillFillDate = autofillFillDate, !Date.isSameDay(Date(), autofillFillDate) else { + return + } + + userDefaults.set(Date(), forKey: Keys.autofillFillDateKey) + + firePixelsFor(.fill) + } + + @objc + private func didReceiveSaveEvent() { + guard !autofillOnboardedUser else { + return + } + + if shouldFireOnboardedUserPixel() { + eventMapping.fire(.autofillOnboardedUser) + } + } + + private func firePixelsFor(_ type: EventType) { + if shouldFireActiveUserPixel() { + eventMapping.fire(.autofillActiveUser) + + if let accountsCountBucket = getAccountsCountBucket() { + eventMapping.fire(.autofillLoginsStacked, parameters: [AutofillPixelEvent.Parameter.countBucket: accountsCountBucket]) + } + + if let cardsCount = try? vault()?.creditCardsCount() { + eventMapping.fire(.autofillCreditCardsStacked, parameters: [AutofillPixelEvent.Parameter.countBucket: creditCardsBucketNameFrom(count: cardsCount)]) + } + } + + switch type { + case .searchDAU: + if shouldFireEnabledUserPixel() { + eventMapping.fire(.autofillEnabledUser) + } + default: + break + } + } + + private func getAccountsCountBucket() -> String? { + if let passwordManager = passwordManager, passwordManager.isEnabled { + // if a user is using a password manager we can't get a count of their passwords so we are assuming they are likely to have a lot of passwords saved + return BucketName.lots.rawValue + } else if let accountsCount = try? vault()?.accountsCount() { + return accountsBucketNameFrom(count: accountsCount) + } + return nil + } + + private func shouldFireActiveUserPixel() -> Bool { + let today = Date() + if Date.isSameDay(today, autofillSearchDauDate) && Date.isSameDay(today, autofillFillDate) { + return true + } + return false + } + + private func shouldFireEnabledUserPixel() -> Bool { + if Date.isSameDay(Date(), autofillSearchDauDate) { + if let passwordManager = passwordManager, passwordManager.isEnabled { + return true + } else if let count = try? vault()?.accountsCount(), count >= 10 { + return true + } + } + return false + } + + private func shouldFireOnboardedUserPixel() -> Bool { + guard !autofillOnboardedUser, let installDate = installDate else { + return false + } + + let pastWeek = Date().addingTimeInterval(.days(-7)) + + if installDate >= pastWeek { + if let passwordManager = passwordManager, passwordManager.isEnabled { + return true + } else if let count = try? vault()?.accountsCount(), count > 0 { + userDefaults.set(true, forKey: Keys.autofillOnboardedUserKey) + return true + } + } else { + userDefaults.set(true, forKey: Keys.autofillOnboardedUserKey) + } + + return false + } + + private func vault() -> (any AutofillSecureVault)? { + if secureVault == nil { + secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: reporter) + } + return secureVault + } + + private func accountsBucketNameFrom(count: Int) -> String { + if count == 0 { + return BucketName.none.rawValue + } else if count < 4 { + return BucketName.few.rawValue + } else if count < 11 { + return BucketName.some.rawValue + } else if count < 50 { + return BucketName.many.rawValue + } else { + return BucketName.lots.rawValue + } + } + + private func creditCardsBucketNameFrom(count: Int) -> String { + if count == 0 { + return BucketName.none.rawValue + } else if count < 4 { + return BucketName.some.rawValue + } else { + return BucketName.many.rawValue + } + } + +} + +public extension NSNotification.Name { + + static let autofillFillEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.AutofillFillEvent") + static let autofillSaveEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.AutofillSaveEvent") + static let searchDAU: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.SearchDAU") + +} diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift index e3a00c4bf..ca5170f5c 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift @@ -721,6 +721,10 @@ extension AutofillUserScript { case autofillPrivateAddress = "autofill_private_address" } + private enum IdentityPixelName: String { + case autofillIdentity = "autofill_identity" + } + /// The pixel name sent by the JS layer. This name does not include the platform on which it was sent. private let originalPixelName: String @@ -739,6 +743,13 @@ extension AutofillUserScript { } } + public var isIdentityPixel: Bool { + if case IdentityPixelName.autofillIdentity.rawValue = originalPixelName { + return true + } + return false + } + public var pixelName: String { switch originalPixelName { case EmailPixelName.autofillPersonalAddress.rawValue: diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift b/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift index 73470a359..70ec30ddc 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift @@ -57,6 +57,7 @@ public protocol AutofillDatabaseProvider: SecureStorageDatabaseProvider { func deleteIdentityForIdentityId(_ identityId: Int64) throws func creditCards() throws -> [SecureVaultModels.CreditCard] + func creditCardsCount() throws -> Int func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? @discardableResult func storeCreditCard(_ creditCard: SecureVaultModels.CreditCard) throws -> Int64 @@ -557,6 +558,13 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro } } + public func creditCardsCount() throws -> Int { + let count = try db.read { + try SecureVaultModels.CreditCard.fetchCount($0) + } + return count + } + public func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { try db.read { return try SecureVaultModels.CreditCard.fetchOne($0, sql: """ diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift b/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift index 498a7a2b7..e411c25b6 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift @@ -87,6 +87,7 @@ public protocol AutofillSecureVault: SecureVault { func deleteIdentityFor(identityId: Int64) throws func creditCards() throws -> [SecureVaultModels.CreditCard] + func creditCardsCount() throws -> Int func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard? @discardableResult @@ -231,15 +232,8 @@ public class DefaultAutofillSecureVault: AutofillSe } public func accountsCount() throws -> Int { - lock.lock() - defer { - lock.unlock() - } - - do { + return try executeThrowingDatabaseOperation { return try self.providers.database.accountsCount() - } catch { - throw SecureStorageError.databaseError(cause: error) } } @@ -526,6 +520,12 @@ public class DefaultAutofillSecureVault: AutofillSe } } + public func creditCardsCount() throws -> Int { + return try executeThrowingDatabaseOperation { + return try self.providers.database.creditCardsCount() + } + } + public func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? { return try executeThrowingDatabaseOperation { guard var card = try self.providers.database.creditCardForCardId(id) else { diff --git a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift index eda3a4569..8fd8f3ebd 100644 --- a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift +++ b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift @@ -246,6 +246,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { if autofilldata.trigger == .passwordGeneration { autogeneratedPassword = data.credentials?.autogenerated ?? false + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) } // Account for cases when the user has manually changed an autogenerated password or private email @@ -616,6 +617,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { var account = SecureVaultModels.WebsiteAccount(username: username, domain: domain, lastUsed: Date()) let credentials = try? vault?.storeWebsiteCredentials(SecureVaultModels.WebsiteCredentials(account: account, password: password)) account.id = String(credentials ?? -1) + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) return account } diff --git a/Sources/Common/Extensions/DateExtension.swift b/Sources/Common/Extensions/DateExtension.swift index 31ca8730c..7bcba129c 100644 --- a/Sources/Common/Extensions/DateExtension.swift +++ b/Sources/Common/Extensions/DateExtension.swift @@ -41,6 +41,11 @@ public extension Date { return Calendar.current.date(byAdding: .day, value: -days, to: Date())! } + static func isSameDay(_ date1: Date, _ date2: Date?) -> Bool { + guard let date2 = date2 else { return false } + return Calendar.current.isDate(date1, inSameDayAs: date2) + } + static var startOfDayTomorrow: Date { let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())! return Calendar.current.startOfDay(for: tomorrow) diff --git a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift index 1e9091456..020f711cd 100644 --- a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift +++ b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift @@ -209,6 +209,11 @@ public final class CredentialsProvider: DataProvider { } else { lastSyncLocalTimestamp = clientTimestamp } + + if !received.isEmpty { + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) + } + syncDidFinish() } diff --git a/Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift b/Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift new file mode 100644 index 000000000..e56a141ef --- /dev/null +++ b/Tests/BrowserServicesKitTests/Autofill/AutofillPixelReporterTests.swift @@ -0,0 +1,421 @@ +// +// AutofillPixelReporterTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import TestUtils +import Common +import SecureStorage +import SecureStorageTestsUtils +@testable import BrowserServicesKit + +final class AutofillPixelReporterTests: XCTestCase { + + private class MockEventMapping: EventMapping { + static var events: [AutofillPixelEvent] = [] + static var loginsParam: String? + static var creditCardsParam: String? + + public init() { + super.init { event, _, param, _ in + Self.events.append(event) + switch event { + case .autofillLoginsStacked: + Self.loginsParam = param?[AutofillPixelEvent.Parameter.countBucket] + case .autofillCreditCardsStacked: + Self.creditCardsParam = param?[AutofillPixelEvent.Parameter.countBucket] + default: + break + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } + } + + private var mockCryptoProvider = MockCryptoProvider() + private var mockDatabaseProvider = (try! MockAutofillDatabaseProvider()) + private var mockKeystoreProvider = MockKeystoreProvider() + private var vault: (any AutofillSecureVault)! + private var eventMapping: MockEventMapping! + private var userDefaults: UserDefaults! + private let testGroupName = "autofill-reporter" + + override func setUpWithError() throws { + try super.setUpWithError() + + userDefaults = UserDefaults(suiteName: testGroupName)! + userDefaults.removePersistentDomain(forName: testGroupName) + + let providers = SecureStorageProviders(crypto: mockCryptoProvider, + database: mockDatabaseProvider, + keystore: mockKeystoreProvider) + + vault = DefaultAutofillSecureVault(providers: providers) + + eventMapping = MockEventMapping() + MockEventMapping.events.removeAll() + } + + override func tearDownWithError() throws { + vault = nil + eventMapping = nil + userDefaults.removePersistentDomain(forName: testGroupName) + + try super.tearDownWithError() + } + + func testWhenFirstFillAndSearchDauIsNotTodayThenNoEventsAreFired() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenFirstFillAndSearchDauIsTodayAndAccountsCountIsZeroThenThreeEventsAreFiredWithNoneParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + setAutofillSearchDauDate(daysAgo: 0) + + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 3) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndFillDateIsNotTodayAndAccountsCountIsZeroThenNoEventsAreFired() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 0) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenFirstSearchDauAndFillDateIsNotTodayAndAndAccountsCountIsTenThenThenOneEventIsFired() throws { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 10) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 1) + let event = try XCTUnwrap(MockEventMapping.events.first) + XCTAssertEqual(event, .autofillEnabledUser) + } + + func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsZeroThenThreeEventsAreFiredWithNoneParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 0) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 3) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsThreeThenTwoEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 3) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 3) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.few.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsTenThenFourEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 10) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 4) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.some.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsElevenThenFourEventsAreFiredWithManyParam() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 11) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 4) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.many.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsFortyThenFourEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 40) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 4) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.many.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndAccountsCountIsFiftyThenFourEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 50) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 4) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillEnabledUser)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.lots.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.none.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndCreditCardsCountIsOneThenThreeEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 0) + createCreditCardsInVault(count: 1) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 3) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.some.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndCreditCardsCountIsThreeThenThreeEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 0) + createCreditCardsInVault(count: 3) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 3) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.some.rawValue) + } + + func testWhenFirstSearchDauAndThenFirstFillAndCreditCardsCountIsFourThenThreeEventsAreFiredWithCorrectParams() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 0) + createCreditCardsInVault(count: 4) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 3) + XCTAssertTrue(MockEventMapping.events.contains(.autofillActiveUser)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillLoginsStacked)) + XCTAssertTrue(MockEventMapping.events.contains(.autofillCreditCardsStacked)) + XCTAssertEqual(MockEventMapping.loginsParam, AutofillPixelReporter.BucketName.none.rawValue) + XCTAssertEqual(MockEventMapping.creditCardsParam, AutofillPixelReporter.BucketName.many.rawValue) + } + + func testWhenSubsequentFillAndSearchDauIsNotTodayThenNoEventsAreFired() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + setAutofillSearchDauDate(daysAgo: 1) + + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenSubsequentFillAndSearchDauIsTodayThenNoEventsAreFired() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + setAutofillSearchDauDate(daysAgo: 0) + setAutofillFillDate(daysAgo: 0) + + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenSubsequentSearchDauAndFillDateIsNotTodayThenNoEventsAreFired() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + setAutofillSearchDauDate(daysAgo: 0) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenSubsequentSearchDauAndFillDateIsTodayThenNoEventsAreFired() { + let autofillPixelReporter = createAutofillPixelReporter() + autofillPixelReporter.resetStoreDefaults() + setAutofillSearchDauDate(daysAgo: 0) + setAutofillFillDate(daysAgo: 0) + + NotificationCenter.default.post(name: .searchDAU, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenSaveAndUserIsAlreadyOnboardedThenOnboardedUserPixelShouldNotBeFired() { + let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-1))) + autofillPixelReporter.resetStoreDefaults() + userDefaults.set(true, forKey: AutofillPixelReporter.Keys.autofillOnboardedUserKey) + + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenSaveAndNotOnboardedAndInstallDateIsNilThenOnboardedUserPixelShouldNotBeFired() { + let autofillPixelReporter = createAutofillPixelReporter(installDate: nil) + autofillPixelReporter.resetStoreDefaults() + + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenUserIsNotOnboardedAndInstallDateIsLessThanSevenDaysAgoAndAccountsCountIsZeroThenOnboardedUserPixelShouldNotBeFired() { + let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-4))) + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 0) + + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + } + + func testWhenUserIsNotOnboardedAndInstallDateIsLessThanSevenDaysAgoAndAccountsCountIsGreaterThanZeroThenOnboardedUserPixelShouldBeFiredAndAutofillOnboardedUserShouldBeTrue() throws { + let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-4))) + autofillPixelReporter.resetStoreDefaults() + createAccountsInVault(count: 1) + + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 1) + let event = try XCTUnwrap(MockEventMapping.events.first) + XCTAssertEqual(event, .autofillOnboardedUser) + let onboardedState = try XCTUnwrap(getAutofillOnboardedUserState()) + XCTAssertTrue(onboardedState) + } + + func testWhenUserIsNotOnboardedAndInstallDateIsGreaterThanSevenDaysAgoThenOnboardedUserPixelShouldNotBeFiredAndAutofillOnboardedUserShouldBeTrue() throws { + let autofillPixelReporter = createAutofillPixelReporter(installDate: Date().addingTimeInterval(.days(-8))) + autofillPixelReporter.resetStoreDefaults() + + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) + + XCTAssertEqual(MockEventMapping.events.count, 0) + let onboardedState = try XCTUnwrap(getAutofillOnboardedUserState()) + XCTAssertTrue(onboardedState) + } + + private func createAutofillPixelReporter(installDate: Date? = Date()) -> AutofillPixelReporter { + return AutofillPixelReporter(userDefaults: userDefaults, + eventMapping: eventMapping, + secureVault: vault, + installDate: installDate) + } + + private func createAccountsInVault(count: Int) { + try? vault.deleteAllWebsiteCredentials() + + for i in 0.. Bool? { + return userDefaults.object(forKey: AutofillPixelReporter.Keys.autofillOnboardedUserKey) as? Bool + } + +} diff --git a/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift b/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift index 7e674a1bf..4c767289e 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift @@ -142,6 +142,10 @@ internal class MockAutofillDatabaseProvider: AutofillDatabaseProvider { return Array(_creditCards.values) } + func creditCardsCount() throws -> Int { + return _creditCards.count + } + func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { return _creditCards[cardId] } From 8d58961b3c51926cc2979b37d1295fafc0859e54 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 23 May 2024 16:04:28 -0400 Subject: [PATCH 10/11] Fix unit tests --- .../Mappers/JsonToRemoteConfigModelMapperTests.swift | 6 +----- .../RemoteMessaging/RemoteMessagingConfigMatcherTests.swift | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift index 4cf9b2bc7..1ea4412f1 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift @@ -131,15 +131,11 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { let rule8 = config.rules.filter { $0.id == 8 }.first XCTAssertNotNil(rule8) XCTAssertNil(rule8?.targetPercentile) - XCTAssertTrue(rule8?.attributes.count == 2) + XCTAssertTrue(rule8?.attributes.count == 1) attribs = rule8?.attributes.filter { $0 is DaysSinceNetPEnabledMatchingAttribute } XCTAssertEqual(attribs?.count, 1) XCTAssertEqual(attribs?.first as? DaysSinceNetPEnabledMatchingAttribute, DaysSinceNetPEnabledMatchingAttribute(min: 5, fallback: nil)) - attribs = rule8?.attributes.filter { $0 is IsNetPWaitlistUserMatchingAttribute } - XCTAssertEqual(attribs?.count, 1) - XCTAssertEqual(attribs?.first as? IsNetPWaitlistUserMatchingAttribute, IsNetPWaitlistUserMatchingAttribute(value: true, fallback: nil)) - let rule9 = config.rules.filter { $0.id == 9 }.first XCTAssertNotNil(rule9) XCTAssertNotNil(rule9?.targetPercentile) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift index 476d416b6..18d832cf0 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift @@ -287,7 +287,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), percentileStore: percentileStore, dismissedMessageIds: []) @@ -318,7 +317,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), percentileStore: percentileStore, dismissedMessageIds: []) @@ -349,7 +347,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), percentileStore: percentileStore, dismissedMessageIds: []) @@ -380,7 +377,6 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, daysSinceNetPEnabled: -1), percentileStore: percentileStore, dismissedMessageIds: []) From 7be3109b91b74ee1d28f0be5dd490d9551731db0 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 24 May 2024 08:55:21 -0700 Subject: [PATCH 11/11] Fix SwiftLint. --- Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 7940ea834..f80f1d4ae 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -56,7 +56,7 @@ public struct UserAttributeMatcher: AttributeMatcher { self.isPrivacyProSubscriber = isPrivacyProSubscriber } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as AppThemeMatchingAttribute: