diff --git a/.gitignore b/.gitignore index 7aafd7b2d..fdb78a2ca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .DS_Store xcuserdata/ .vscode +*.swift.plist diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift index 56caf5c64..981db0ee1 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift @@ -21,8 +21,12 @@ import Foundation struct JsonToRemoteConfigModelMapper { - static func mapJson(remoteMessagingConfig: RemoteMessageResponse.JsonRemoteMessagingConfig) -> RemoteConfigModel { - let remoteMessages = JsonToRemoteMessageModelMapper.maps(jsonRemoteMessages: remoteMessagingConfig.messages) + static func mapJson(remoteMessagingConfig: RemoteMessageResponse.JsonRemoteMessagingConfig, + surveyActionMapper: RemoteMessagingSurveyActionMapping) -> RemoteConfigModel { + let remoteMessages = JsonToRemoteMessageModelMapper.maps( + jsonRemoteMessages: remoteMessagingConfig.messages, + surveyActionMapper: surveyActionMapper + ) os_log("remoteMessages mapped = %s", log: .remoteMessaging, type: .debug, String(describing: remoteMessages)) let rules = JsonToRemoteMessageModelMapper.maps(jsonRemoteRules: remoteMessagingConfig.rules) os_log("rules mapped = %s", log: .remoteMessaging, type: .debug, String(describing: rules)) diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index 3a50177c6..c84074299 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -37,6 +37,8 @@ private enum AttributesKey: String, CaseIterable { case appTheme case daysSinceInstalled case daysSinceNetPEnabled + case pproEligible + case pproSubscriber func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { switch self { @@ -56,6 +58,8 @@ private enum AttributesKey: String, CaseIterable { case .appTheme: return AppThemeMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .daysSinceInstalled: return DaysSinceInstalledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .daysSinceNetPEnabled: return DaysSinceNetPEnabledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .pproEligible: return IsPrivacyProEligibleUserMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .pproSubscriber: return IsPrivacyProSubscriberUserMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } } @@ -63,11 +67,16 @@ private enum AttributesKey: String, CaseIterable { struct JsonToRemoteMessageModelMapper { - static func maps(jsonRemoteMessages: [RemoteMessageResponse.JsonRemoteMessage]) -> [RemoteMessageModel] { + static func maps(jsonRemoteMessages: [RemoteMessageResponse.JsonRemoteMessage], + surveyActionMapper: RemoteMessagingSurveyActionMapping) -> [RemoteMessageModel] { var remoteMessages: [RemoteMessageModel] = [] jsonRemoteMessages.forEach { message in + guard let content = mapToContent( content: message.content, surveyActionMapper: surveyActionMapper) else { + return + } + var remoteMessage = RemoteMessageModel(id: message.id, - content: mapToContent(content: message.content), + content: content, matchingRules: message.matchingRules ?? [], exclusionRules: message.exclusionRules ?? []) @@ -81,7 +90,8 @@ struct JsonToRemoteMessageModelMapper { } // swiftlint:disable cyclomatic_complexity function_body_length - static func mapToContent(content: RemoteMessageResponse.JsonContent) -> RemoteMessageModelType? { + static func mapToContent(content: RemoteMessageResponse.JsonContent, + surveyActionMapper: RemoteMessagingSurveyActionMapping) -> RemoteMessageModelType? { switch RemoteMessageResponse.JsonMessageType(rawValue: content.messageType) { case .small: guard !content.titleText.isEmpty, !content.descriptionText.isEmpty else { @@ -101,7 +111,7 @@ struct JsonToRemoteMessageModelMapper { case .bigSingleAction: guard let primaryActionText = content.primaryActionText, !primaryActionText.isEmpty, - let action = mapToAction(content.primaryAction) + let action = mapToAction(content.primaryAction, surveyActionMapper: surveyActionMapper) else { return nil } @@ -114,10 +124,10 @@ struct JsonToRemoteMessageModelMapper { case .bigTwoAction: guard let primaryActionText = content.primaryActionText, !primaryActionText.isEmpty, - let primaryAction = mapToAction(content.primaryAction), + let primaryAction = mapToAction(content.primaryAction, surveyActionMapper: surveyActionMapper), let secondaryActionText = content.secondaryActionText, !secondaryActionText.isEmpty, - let secondaryAction = mapToAction(content.secondaryAction) + let secondaryAction = mapToAction(content.secondaryAction, surveyActionMapper: surveyActionMapper) else { return nil } @@ -132,7 +142,7 @@ struct JsonToRemoteMessageModelMapper { case .promoSingleAction: guard let actionText = content.actionText, !actionText.isEmpty, - let action = mapToAction(content.action) + let action = mapToAction(content.action, surveyActionMapper: surveyActionMapper) else { return nil } @@ -149,7 +159,8 @@ struct JsonToRemoteMessageModelMapper { } // swiftlint:enable cyclomatic_complexity function_body_length - static func mapToAction(_ jsonAction: RemoteMessageResponse.JsonMessageAction?) -> RemoteAction? { + static func mapToAction(_ jsonAction: RemoteMessageResponse.JsonMessageAction?, + surveyActionMapper: RemoteMessagingSurveyActionMapping) -> RemoteAction? { guard let jsonAction = jsonAction else { return nil } @@ -159,8 +170,23 @@ struct JsonToRemoteMessageModelMapper { return .share(value: jsonAction.value, title: jsonAction.additionalParameters?["title"]) case .url: return .url(value: jsonAction.value) - case .surveyURL: - return .surveyURL(value: jsonAction.value) + case .survey: + if let queryParamsString = jsonAction.additionalParameters?["queryParams"] as? String { + let queryParams = queryParamsString.components(separatedBy: ";") + let mappedQueryParams = queryParams.compactMap { param in + return RemoteMessagingSurveyActionParameter(rawValue: param) + } + + if mappedQueryParams.count == queryParams.count, let surveyURL = URL(string: jsonAction.value) { + let updatedURL = surveyActionMapper.add(parameters: mappedQueryParams, to: surveyURL) + return .survey(value: updatedURL.absoluteString) + } else { + // The message requires a parameter that isn't supported + return nil + } + } else { + return .survey(value: jsonAction.value) + } case .appStore: return .appStore case .dismiss: diff --git a/Sources/RemoteMessaging/Mappers/RemoteMessagingSurveyActionMapping.swift b/Sources/RemoteMessaging/Mappers/RemoteMessagingSurveyActionMapping.swift new file mode 100644 index 000000000..243ee6a79 --- /dev/null +++ b/Sources/RemoteMessaging/Mappers/RemoteMessagingSurveyActionMapping.swift @@ -0,0 +1,35 @@ +// +// RemoteMessagingSurveyActionMapping.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 + +public enum RemoteMessagingSurveyActionParameter: String, CaseIterable { + case appVersion = "ddgv" + case atb = "atb" + case atbVariant = "var" + case daysInstalled = "delta" + case hardwareModel = "mo" + case lastActiveDate = "da" + case osVersion = "osv" +} + +public protocol RemoteMessagingSurveyActionMapping { + + func add(parameters: [RemoteMessagingSurveyActionParameter], to url: URL) -> URL + +} diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index ac57d3532..7940ea834 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -30,6 +30,8 @@ public struct UserAttributeMatcher: AttributeMatcher { private let favoritesCount: Int private let isWidgetInstalled: Bool private let daysSinceNetPEnabled: Int + private let isPrivacyProEligibleUser: Bool + private let isPrivacyProSubscriber: Bool public init(statisticsStore: StatisticsStore, variantManager: VariantManager, @@ -38,7 +40,9 @@ public struct UserAttributeMatcher: AttributeMatcher { favoritesCount: Int, appTheme: String, isWidgetInstalled: Bool, - daysSinceNetPEnabled: Int + daysSinceNetPEnabled: Int, + isPrivacyProEligibleUser: Bool, + isPrivacyProSubscriber: Bool ) { self.statisticsStore = statisticsStore self.variantManager = variantManager @@ -48,6 +52,8 @@ public struct UserAttributeMatcher: AttributeMatcher { self.favoritesCount = favoritesCount self.isWidgetInstalled = isWidgetInstalled self.daysSinceNetPEnabled = daysSinceNetPEnabled + self.isPrivacyProEligibleUser = isPrivacyProEligibleUser + self.isPrivacyProSubscriber = isPrivacyProSubscriber } // swiftlint:disable:next cyclomatic_complexity @@ -100,6 +106,18 @@ public struct UserAttributeMatcher: AttributeMatcher { } else { return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: daysSinceNetPEnabled) } + case let matchingAttribute as IsPrivacyProEligibleUserMatchingAttribute: + guard let value = matchingAttribute.value else { + return .fail + } + + return BooleanMatchingAttribute(value).matches(value: isPrivacyProEligibleUser) + case let matchingAttribute as IsPrivacyProSubscriberUserMatchingAttribute: + guard let value = matchingAttribute.value else { + return .fail + } + + return BooleanMatchingAttribute(value).matches(value: isPrivacyProSubscriber) default: assertionFailure("Could not find matching attribute") return nil diff --git a/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift b/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift index eda32d920..78b3e2a24 100644 --- a/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift +++ b/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift @@ -87,7 +87,7 @@ public enum RemoteMessageResponse { case url case appStore = "appstore" case dismiss - case surveyURL = "survey_url" + case survey = "survey" } enum JsonPlaceholder: String, CaseIterable { diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index c1906a31e..7fc275706 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -646,6 +646,56 @@ struct DaysSinceNetPEnabledMatchingAttribute: MatchingAttribute, Equatable { } } +struct IsPrivacyProEligibleUserMatchingAttribute: 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: IsPrivacyProEligibleUserMatchingAttribute, rhs: IsPrivacyProEligibleUserMatchingAttribute) -> Bool { + return lhs.value == rhs.value && lhs.fallback == rhs.fallback + } +} + +struct IsPrivacyProSubscriberUserMatchingAttribute: 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: IsPrivacyProSubscriberUserMatchingAttribute, rhs: IsPrivacyProSubscriberUserMatchingAttribute) -> Bool { + return lhs.value == rhs.value && lhs.fallback == rhs.fallback + } +} + enum MatchingAttributeDefaults { static let intDefaultValue = -1 static let intDefaultMaxValue = Int.max diff --git a/Sources/RemoteMessaging/Model/RemoteMessageModel.swift b/Sources/RemoteMessaging/Model/RemoteMessageModel.swift index 2bf6a39e0..2c0773ad6 100644 --- a/Sources/RemoteMessaging/Model/RemoteMessageModel.swift +++ b/Sources/RemoteMessaging/Model/RemoteMessageModel.swift @@ -102,7 +102,7 @@ public enum RemoteMessageModelType: Codable, Equatable { public enum RemoteAction: Codable, Equatable { case share(value: String, title: String?) case url(value: String) - case surveyURL(value: String) + case survey(value: String) case appStore case dismiss } diff --git a/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift b/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift index 23d4c9471..4df18d7a6 100644 --- a/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift +++ b/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift @@ -26,6 +26,7 @@ public struct RemoteMessagingConfigMatcher { private let userAttributeMatcher: UserAttributeMatcher private let percentileStore: RemoteMessagingPercentileStoring private let dismissedMessageIds: [String] + let surveyActionMapper: RemoteMessagingSurveyActionMapping private let matchers: [AttributeMatcher] @@ -33,11 +34,13 @@ public struct RemoteMessagingConfigMatcher { deviceAttributeMatcher: DeviceAttributeMatcher = DeviceAttributeMatcher(), userAttributeMatcher: UserAttributeMatcher, percentileStore: RemoteMessagingPercentileStoring, + surveyActionMapper: RemoteMessagingSurveyActionMapping, dismissedMessageIds: [String]) { self.appAttributeMatcher = appAttributeMatcher self.deviceAttributeMatcher = deviceAttributeMatcher self.userAttributeMatcher = userAttributeMatcher self.percentileStore = percentileStore + self.surveyActionMapper = surveyActionMapper self.dismissedMessageIds = dismissedMessageIds matchers = [appAttributeMatcher, deviceAttributeMatcher, userAttributeMatcher] diff --git a/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift b/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift index 233864732..e8afc324d 100644 --- a/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift +++ b/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift @@ -42,7 +42,10 @@ public struct RemoteMessagingConfigProcessor { let isNewVersion = newVersion != currentVersion if isNewVersion || shouldProcessConfig(currentConfig) { - let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: jsonRemoteMessagingConfig) + let config = JsonToRemoteConfigModelMapper.mapJson( + remoteMessagingConfig: jsonRemoteMessagingConfig, + surveyActionMapper: remoteMessagingConfigMatcher.surveyActionMapper + ) let message = remoteMessagingConfigMatcher.evaluate(remoteConfig: config) os_log("Message to present next: %s", log: .remoteMessaging, type: .debug, message.debugDescription) diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift index 1ea4412f1..f752066a3 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift @@ -92,7 +92,7 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { descriptionText: "Survey Description", placeholder: .vpnAnnounce, actionText: "Survey Action", - action: .surveyURL(value: "https://duckduckgo.com/survey") + action: .survey(value: "https://duckduckgo.com/survey") ), matchingRules: [8], exclusionRules: []) @@ -131,11 +131,25 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { let rule8 = config.rules.filter { $0.id == 8 }.first XCTAssertNotNil(rule8) XCTAssertNil(rule8?.targetPercentile) - XCTAssertTrue(rule8?.attributes.count == 1) + XCTAssertTrue(rule8?.attributes.count == 3) 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 IsPrivacyProEligibleUserMatchingAttribute } + XCTAssertEqual(attribs?.count, 1) + XCTAssertEqual( + attribs?.first as? IsPrivacyProEligibleUserMatchingAttribute, + IsPrivacyProEligibleUserMatchingAttribute(value: true, fallback: nil) + ) + + attribs = rule8?.attributes.filter { $0 is IsPrivacyProSubscriberUserMatchingAttribute } + XCTAssertEqual(attribs?.count, 1) + XCTAssertEqual( + attribs?.first as? IsPrivacyProSubscriberUserMatchingAttribute, + IsPrivacyProSubscriberUserMatchingAttribute(value: true, fallback: nil) + ) + let rule9 = config.rules.filter { $0.id == 9 }.first XCTAssertNotNil(rule9) XCTAssertNotNil(rule9?.targetPercentile) @@ -169,8 +183,9 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { func testWhenJsonAttributeMissingThenUnknownIntoConfig() throws { let validJson = data.fromJsonFile("Resources/remote-messaging-config-malformed.json") let remoteMessagingConfig = try JSONDecoder().decode(RemoteMessageResponse.JsonRemoteMessagingConfig.self, from: validJson) + let surveyMapper = MockRemoteMessageSurveyActionMapper() XCTAssertNotNil(remoteMessagingConfig) - let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: remoteMessagingConfig) + let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: remoteMessagingConfig, surveyActionMapper: surveyMapper) XCTAssertTrue(config.rules.count == 2) let rule6 = config.rules.filter { $0.id == 6 }.first @@ -183,9 +198,10 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { func decodeAndMapJson(fileName: String) throws -> RemoteConfigModel { let validJson = data.fromJsonFile(fileName) let remoteMessagingConfig = try JSONDecoder().decode(RemoteMessageResponse.JsonRemoteMessagingConfig.self, from: validJson) + let surveyMapper = MockRemoteMessageSurveyActionMapper() XCTAssertNotNil(remoteMessagingConfig) - let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: remoteMessagingConfig) + let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: remoteMessagingConfig, surveyActionMapper: surveyMapper) XCTAssertNotNil(config) return config } diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift index 80881eb72..683874057 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift @@ -53,7 +53,9 @@ class UserAttributeMatcherTests: XCTestCase { favoritesCount: 88, appTheme: "default", isWidgetInstalled: true, - daysSinceNetPEnabled: 3) + daysSinceNetPEnabled: 3, + isPrivacyProEligibleUser: true, + isPrivacyProSubscriber: true) } override func tearDownWithError() throws { @@ -204,7 +206,7 @@ class UserAttributeMatcherTests: XCTestCase { .fail) } - // MARK: - Network Protection Waitlist + // MARK: - Privacy Pro func testWhenDaysSinceNetPEnabledMatchesThenReturnMatch() throws { XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: DaysSinceNetPEnabledMatchingAttribute(min: 1, fallback: nil)), @@ -216,4 +218,24 @@ class UserAttributeMatcherTests: XCTestCase { .fail) } + func testWhenIsPrivacyProEligibleUserMatchesThenReturnMatch() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsPrivacyProEligibleUserMatchingAttribute(value: true, fallback: nil)), + .match) + } + + func testWhenIsPrivacyProEligibleUserDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsPrivacyProEligibleUserMatchingAttribute(value: false, fallback: nil)), + .fail) + } + + func testWhenIsPrivacyProSubscriberMatchesThenReturnMatch() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsPrivacyProSubscriberUserMatchingAttribute(value: true, fallback: nil)), + .match) + } + + func testWhenIsPrivacyProSubscriberDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsPrivacyProSubscriberUserMatchingAttribute(value: false, fallback: nil)), + .fail) + } + } diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mocks/MockRemoteMessageSurveyActionMapper.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mocks/MockRemoteMessageSurveyActionMapper.swift new file mode 100644 index 000000000..07f9ec4d0 --- /dev/null +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mocks/MockRemoteMessageSurveyActionMapper.swift @@ -0,0 +1,28 @@ +// +// MockRemoteMessageSurveyActionMapper.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 RemoteMessaging + +class MockRemoteMessageSurveyActionMapper: RemoteMessagingSurveyActionMapping { + + func add(parameters: [RemoteMessaging.RemoteMessagingSurveyActionParameter], to url: URL) -> URL { + return url + } + +} diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift index 18d832cf0..ebaf2060c 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift @@ -43,8 +43,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: MockRemoteMessagePercentileStore(), + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: [] ) } @@ -126,8 +129,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: MockRemoteMessagePercentileStore(), + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [ @@ -219,8 +225,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: MockRemoteMessagePercentileStore(), + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: ["1"]) let remoteConfig = RemoteConfigModel(messages: [ @@ -255,8 +264,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: MockRemoteMessagePercentileStore(), + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [ @@ -287,8 +299,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: percentileStore, + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [ @@ -317,8 +332,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: percentileStore, + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [ @@ -347,8 +365,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: percentileStore, + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [ @@ -377,8 +398,11 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: percentileStore, + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [ @@ -427,9 +451,10 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { func decodeAndMapJson(fileName: String) throws -> RemoteConfigModel { let validJson = data.fromJsonFile(fileName) let remoteMessagingConfig = try JSONDecoder().decode(RemoteMessageResponse.JsonRemoteMessagingConfig.self, from: validJson) + let surveyMapper = MockRemoteMessageSurveyActionMapper() XCTAssertNotNil(remoteMessagingConfig) - let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: remoteMessagingConfig) + let config = JsonToRemoteConfigModelMapper.mapJson(remoteMessagingConfig: remoteMessagingConfig, surveyActionMapper: surveyMapper) XCTAssertNotNil(config) return config } diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift index ad6e21819..bbfa2dd59 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift @@ -36,8 +36,11 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: MockRemoteMessagePercentileStore(), + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: [] ) @@ -64,8 +67,11 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: MockRemoteMessagePercentileStore(), + surveyActionMapper: MockRemoteMessageSurveyActionMapper(), dismissedMessageIds: []) let processor = RemoteMessagingConfigProcessor(remoteMessagingConfigMatcher: remoteMessagingConfigMatcher) diff --git a/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json b/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json index a4defd79d..e75d3a15d 100644 --- a/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json +++ b/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json @@ -134,6 +134,23 @@ } } }, + { + "id": "9848E904-4345-09C8-FEAA-3B2C75DC285B", + "content": { + "messageType": "promo_single_action", + "titleText": "Survey Title", + "descriptionText": "Survey Description", + "placeholder": "VPNAnnounce", + "actionText": "Survey Action", + "action": { + "type": "survey", + "value": "https://duckduckgo.com/survey", + "additionalParameters": { + "queryParams": "atb;var;delta;osv;ddgv;da;unknown_param_which_is_not_supported" + } + } + } + }, { "id": "8E909844-C809-4543-AAFE-2C75DC285B3B", "content": { @@ -143,8 +160,11 @@ "placeholder": "VPNAnnounce", "actionText": "Survey Action", "action": { - "type": "survey_url", - "value": "https://duckduckgo.com/survey" + "type": "survey", + "value": "https://duckduckgo.com/survey", + "additionalParameters": { + "queryParams": "atb;var;delta;osv;ddgv;da" + } } }, "matchingRules": [ @@ -239,6 +259,12 @@ "attributes": { "daysSinceNetPEnabled": { "min": 5 + }, + "pproEligible": { + "value": true + }, + "pproSubscriber": { + "value": true } } },