From fefe3d2548bda12fe5f215eb608795e02bd3b684 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 13:53:59 +0200 Subject: [PATCH 01/12] Add IsInstalledMacAppStore matcher and refactor most of the existing matchers --- .../JsonToRemoteMessageModelMapper.swift | 2 + .../Matchers/UserAttributeMatcher.swift | 64 +- .../Model/MatchingAttributes.swift | 622 ++---------------- .../Model/NumericRangeMatchingAttribute.swift | 54 ++ .../Model/SingleValueMatchingAttribute.swift | 42 ++ 5 files changed, 229 insertions(+), 555 deletions(-) create mode 100644 Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift create mode 100644 Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index d02be631c..b85ba5cbf 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -43,6 +43,7 @@ private enum AttributesKey: String, CaseIterable { case pproPurchasePlatform case pproSubscriptionStatus case interactedWithMessage + case isInstalledMacAppStore func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { switch self { @@ -69,6 +70,7 @@ private enum AttributesKey: String, CaseIterable { case .pproPurchasePlatform: return PrivacyProPurchasePlatformMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .pproSubscriptionStatus: return PrivacyProSubscriptionStatusMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .interactedWithMessage: return InteractedWithMessageMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .isInstalledMacAppStore: return IsInstalledMacAppStoreMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } } diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 299dbe278..6cd8ec2e1 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -26,8 +26,6 @@ public typealias UserAttributeMatcher = MobileUserAttributeMatcher public typealias UserAttributeMatcher = DesktopUserAttributeMatcher #endif -public typealias DesktopUserAttributeMatcher = CommonUserAttributeMatcher - public struct MobileUserAttributeMatcher: AttributeMatching { private enum PrivacyProSubscriptionStatus: String { @@ -95,6 +93,66 @@ public struct MobileUserAttributeMatcher: AttributeMatching { } +public struct DesktopUserAttributeMatcher: AttributeMatching { + private let isInstalledMacAppStore: Bool + + private let commonUserAttributeMatcher: CommonUserAttributeMatcher + + public init(statisticsStore: StatisticsStore, + variantManager: VariantManager, + emailManager: EmailManager = EmailManager(), + bookmarksCount: Int, + favoritesCount: Int, + appTheme: String, + daysSinceNetPEnabled: Int, + isPrivacyProEligibleUser: Bool, + isPrivacyProSubscriber: Bool, + privacyProDaysSinceSubscribed: Int, + privacyProDaysUntilExpiry: Int, + privacyProPurchasePlatform: String?, + isPrivacyProSubscriptionActive: Bool, + isPrivacyProSubscriptionExpiring: Bool, + isPrivacyProSubscriptionExpired: Bool, + dismissedMessageIds: [String], + isInstalledMacAppStore: Bool + ) { + self.isInstalledMacAppStore = isInstalledMacAppStore + + commonUserAttributeMatcher = .init( + statisticsStore: statisticsStore, + variantManager: variantManager, + emailManager: emailManager, + bookmarksCount: bookmarksCount, + favoritesCount: favoritesCount, + appTheme: appTheme, + daysSinceNetPEnabled: daysSinceNetPEnabled, + isPrivacyProEligibleUser: isPrivacyProEligibleUser, + isPrivacyProSubscriber: isPrivacyProSubscriber, + privacyProDaysSinceSubscribed: privacyProDaysSinceSubscribed, + privacyProDaysUntilExpiry: privacyProDaysUntilExpiry, + privacyProPurchasePlatform: privacyProPurchasePlatform, + isPrivacyProSubscriptionActive: isPrivacyProSubscriptionActive, + isPrivacyProSubscriptionExpiring: isPrivacyProSubscriptionExpiring, + isPrivacyProSubscriptionExpired: isPrivacyProSubscriptionExpired, + dismissedMessageIds: dismissedMessageIds + ) + } + + public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { + switch matchingAttribute { + case let matchingAttribute as IsInstalledMacAppStoreMatchingAttribute: + guard let value = matchingAttribute.value else { + return .fail + } + + return BooleanMatchingAttribute(value).matches(value: isInstalledMacAppStore) + default: + return commonUserAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) + } + } +} + + public struct CommonUserAttributeMatcher: AttributeMatching { private enum PrivacyProSubscriptionStatus: String { @@ -226,7 +284,7 @@ public struct CommonUserAttributeMatcher: AttributeMatching { case let matchingAttribute as PrivacyProPurchasePlatformMatchingAttribute: return StringArrayMatchingAttribute(matchingAttribute.value).matches(value: privacyProPurchasePlatform ?? "") case let matchingAttribute as PrivacyProSubscriptionStatusMatchingAttribute: - let mappedStatuses = matchingAttribute.value.compactMap { status in + let mappedStatuses = (matchingAttribute.value ?? []).compactMap { status in return PrivacyProSubscriptionStatus(rawValue: status) } diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index f31ec79a9..583fcdd8a 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -19,7 +19,7 @@ import Foundation import Common -private enum RuleAttributes { +enum RuleAttributes { static let min = "min" static let max = "max" static let value = "value" @@ -31,33 +31,13 @@ public protocol MatchingAttribute { var fallback: Bool? { get } } -struct LocaleMatchingAttribute: MatchingAttribute, Equatable { - var value: [String] = [] +struct LocaleMatchingAttribute: SingleValueMatchingAttribute { + var value: [String]? = [] var fallback: Bool? - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { - return - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? [String] { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: [String], fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: LocaleMatchingAttribute, rhs: LocaleMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } - static func localeIdentifierAsJsonFormat(_ localeIdentifier: String) -> String { - return localeIdentifier.replacingOccurrences(of: "_", with: "-") + let baseIdentifier = localeIdentifier.components(separatedBy: "@").first ?? localeIdentifier + return baseIdentifier.replacingOccurrences(of: "_", with: "-") } } @@ -99,54 +79,14 @@ struct OSMatchingAttribute: MatchingAttribute, Equatable { } } -struct IsInternalUserMatchingAttribute: MatchingAttribute, Equatable { +struct IsInternalUserMatchingAttribute: SingleValueMatchingAttribute { 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: IsInternalUserMatchingAttribute, rhs: IsInternalUserMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct AppIdMatchingAttribute: MatchingAttribute, Equatable { +struct AppIdMatchingAttribute: SingleValueMatchingAttribute { var value: String? var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: String?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: AppIdMatchingAttribute, rhs: AppIdMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } struct AppVersionMatchingAttribute: MatchingAttribute, Equatable { @@ -187,293 +127,111 @@ struct AppVersionMatchingAttribute: MatchingAttribute, Equatable { } } -struct AtbMatchingAttribute: MatchingAttribute, Equatable { +struct AtbMatchingAttribute: SingleValueMatchingAttribute { var value: String? var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: String?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: AtbMatchingAttribute, rhs: AtbMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct AppAtbMatchingAttribute: MatchingAttribute, Equatable { +struct AppAtbMatchingAttribute: SingleValueMatchingAttribute { var value: String? var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: String?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: AppAtbMatchingAttribute, rhs: AppAtbMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct SearchAtbMatchingAttribute: MatchingAttribute, Equatable { +struct SearchAtbMatchingAttribute: SingleValueMatchingAttribute { var value: String? var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: String?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: SearchAtbMatchingAttribute, rhs: SearchAtbMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct ExpVariantMatchingAttribute: MatchingAttribute, Equatable { +struct ExpVariantMatchingAttribute: SingleValueMatchingAttribute { var value: String? var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: String?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: ExpVariantMatchingAttribute, rhs: ExpVariantMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct EmailEnabledMatchingAttribute: MatchingAttribute, Equatable { +struct EmailEnabledMatchingAttribute: SingleValueMatchingAttribute { 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: EmailEnabledMatchingAttribute, rhs: EmailEnabledMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct WidgetAddedMatchingAttribute: MatchingAttribute, Equatable { +struct WidgetAddedMatchingAttribute: SingleValueMatchingAttribute { 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: WidgetAddedMatchingAttribute, rhs: WidgetAddedMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct BookmarksMatchingAttribute: MatchingAttribute, Equatable { +struct BookmarksMatchingAttribute: NumericRangeMatchingAttribute { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: Int = MatchingAttributeDefaults.intDefaultValue, - max: Int = MatchingAttributeDefaults.intDefaultMaxValue, - value: Int = MatchingAttributeDefaults.intDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: BookmarksMatchingAttribute, rhs: BookmarksMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct FavoritesMatchingAttribute: MatchingAttribute, Equatable { +struct FavoritesMatchingAttribute: NumericRangeMatchingAttribute { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: Int = MatchingAttributeDefaults.intDefaultValue, - max: Int = MatchingAttributeDefaults.intDefaultMaxValue, - value: Int = MatchingAttributeDefaults.intDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: FavoritesMatchingAttribute, rhs: FavoritesMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } -struct AppThemeMatchingAttribute: MatchingAttribute, Equatable { +struct AppThemeMatchingAttribute: SingleValueMatchingAttribute { var value: String? var fallback: Bool? +} - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } +struct DaysSinceInstalledMatchingAttribute: NumericRangeMatchingAttribute { + var min: Int = MatchingAttributeDefaults.intDefaultValue + var max: Int = MatchingAttributeDefaults.intDefaultMaxValue + var value: Int = MatchingAttributeDefaults.intDefaultValue + var fallback: Bool? +} - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } +struct DaysSinceNetPEnabledMatchingAttribute: NumericRangeMatchingAttribute { + var min: Int = MatchingAttributeDefaults.intDefaultValue + var max: Int = MatchingAttributeDefaults.intDefaultMaxValue + var value: Int = MatchingAttributeDefaults.intDefaultValue + var fallback: Bool? +} - init(value: String?, fallback: Bool?) { - self.value = value - self.fallback = fallback - } +struct IsPrivacyProEligibleUserMatchingAttribute: SingleValueMatchingAttribute { + var value: Bool? + var fallback: Bool? +} - static func == (lhs: AppThemeMatchingAttribute, rhs: AppThemeMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } +struct IsPrivacyProSubscriberUserMatchingAttribute: SingleValueMatchingAttribute { + var value: Bool? + var fallback: Bool? } -struct DaysSinceInstalledMatchingAttribute: MatchingAttribute, Equatable { +struct PrivacyProDaysSinceSubscribedMatchingAttribute: NumericRangeMatchingAttribute { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? +} - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } +struct PrivacyProDaysUntilExpiryMatchingAttribute: NumericRangeMatchingAttribute { + var min: Int = MatchingAttributeDefaults.intDefaultValue + var max: Int = MatchingAttributeDefaults.intDefaultMaxValue + var value: Int = MatchingAttributeDefaults.intDefaultValue + var fallback: Bool? +} - if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } +struct PrivacyProPurchasePlatformMatchingAttribute: SingleValueMatchingAttribute { + var value: [String]? = [] + var fallback: Bool? +} - init(min: Int = MatchingAttributeDefaults.intDefaultValue, - max: Int = MatchingAttributeDefaults.intDefaultMaxValue, - value: Int = MatchingAttributeDefaults.intDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } +struct PrivacyProSubscriptionStatusMatchingAttribute: SingleValueMatchingAttribute { + var value: [String]? = [] + var fallback: Bool? +} - static func == (lhs: DaysSinceInstalledMatchingAttribute, rhs: DaysSinceInstalledMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } +struct InteractedWithMessageMatchingAttribute: SingleValueMatchingAttribute { + var value: [String]? = [] + var fallback: Bool? +} + +struct IsInstalledMacAppStoreMatchingAttribute: SingleValueMatchingAttribute { + var value: Bool? + var fallback: Bool? } struct UnknownMatchingAttribute: MatchingAttribute, Equatable { @@ -496,6 +254,14 @@ struct UnknownMatchingAttribute: MatchingAttribute, Equatable { } } +enum MatchingAttributeDefaults { + static let intDefaultValue = -1 + static let intDefaultMaxValue = Int.max + static let stringDefaultValue = "" +} + +// MARK: - + struct BooleanMatchingAttribute: Equatable { var value: Bool @@ -560,8 +326,8 @@ struct StringMatchingAttribute: Equatable { struct StringArrayMatchingAttribute: Equatable { var values: [String] - init(_ values: [String]) { - self.values = values.map { $0.lowercased() } + init(_ values: [String]?) { + self.values = (values ?? []).map { $0.lowercased() } } func matches(value: String) -> EvaluationResult { @@ -607,251 +373,3 @@ struct RangeStringNumericMatchingAttribute: Equatable { return lhs.min == rhs.min && lhs.max == rhs.max } } - -struct DaysSinceNetPEnabledMatchingAttribute: MatchingAttribute, Equatable { - var min: Int = MatchingAttributeDefaults.intDefaultValue - var max: Int = MatchingAttributeDefaults.intDefaultMaxValue - var value: Int = MatchingAttributeDefaults.intDefaultValue - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: Int = MatchingAttributeDefaults.intDefaultValue, - max: Int = MatchingAttributeDefaults.intDefaultMaxValue, - value: Int = MatchingAttributeDefaults.intDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: DaysSinceNetPEnabledMatchingAttribute, rhs: DaysSinceNetPEnabledMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - -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 - } -} - -struct PrivacyProDaysSinceSubscribedMatchingAttribute: MatchingAttribute, Equatable { - var min: Int = MatchingAttributeDefaults.intDefaultValue - var max: Int = MatchingAttributeDefaults.intDefaultMaxValue - var value: Int = MatchingAttributeDefaults.intDefaultValue - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: Int = MatchingAttributeDefaults.intDefaultValue, - max: Int = MatchingAttributeDefaults.intDefaultMaxValue, - value: Int = MatchingAttributeDefaults.intDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: PrivacyProDaysSinceSubscribedMatchingAttribute, rhs: PrivacyProDaysSinceSubscribedMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - -struct PrivacyProDaysUntilExpiryMatchingAttribute: MatchingAttribute, Equatable { - var min: Int = MatchingAttributeDefaults.intDefaultValue - var max: Int = MatchingAttributeDefaults.intDefaultMaxValue - var value: Int = MatchingAttributeDefaults.intDefaultValue - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: Int = MatchingAttributeDefaults.intDefaultValue, - max: Int = MatchingAttributeDefaults.intDefaultMaxValue, - value: Int = MatchingAttributeDefaults.intDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: PrivacyProDaysUntilExpiryMatchingAttribute, rhs: PrivacyProDaysUntilExpiryMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - -struct PrivacyProPurchasePlatformMatchingAttribute: MatchingAttribute, Equatable { - var value: [String] = [] - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { - return - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? [String] { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: [String], fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: PrivacyProPurchasePlatformMatchingAttribute, rhs: PrivacyProPurchasePlatformMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - -struct PrivacyProSubscriptionStatusMatchingAttribute: MatchingAttribute, Equatable { - var value: [String] = [] - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { - return - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? [String] { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: [String], fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: PrivacyProSubscriptionStatusMatchingAttribute, rhs: PrivacyProSubscriptionStatusMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - -struct InteractedWithMessageMatchingAttribute: MatchingAttribute, Equatable { - var value: [String] = [] - var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { - return - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? [String] { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(value: [String], fallback: Bool?) { - self.value = value - self.fallback = fallback - } - - static func == (lhs: InteractedWithMessageMatchingAttribute, rhs: InteractedWithMessageMatchingAttribute) -> Bool { - return lhs.value == rhs.value && lhs.fallback == rhs.fallback - } -} - -enum MatchingAttributeDefaults { - static let intDefaultValue = -1 - static let intDefaultMaxValue = Int.max - static let stringDefaultValue = "" -} diff --git a/Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift b/Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift new file mode 100644 index 000000000..0de0a9e48 --- /dev/null +++ b/Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift @@ -0,0 +1,54 @@ +// +// NumericRangeMatchingAttribute.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 protocol NumericRangeMatchingAttribute: MatchingAttribute, Equatable { + var min: Int { get set } + var max: Int { get set } + var value: Int { get set } + var fallback: Bool? { get set } + + init(min: Int, max: Int, value: Int, fallback: Bool?) +} + +public extension NumericRangeMatchingAttribute { + + init(jsonMatchingAttribute: AnyDecodable) { + guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { + self.init(fallback: nil) + return + } + + let min = jsonMatchingAttribute[RuleAttributes.min] as? Int ?? MatchingAttributeDefaults.intDefaultValue + let max = jsonMatchingAttribute[RuleAttributes.max] as? Int ?? MatchingAttributeDefaults.intDefaultMaxValue + let value = jsonMatchingAttribute[RuleAttributes.value] as? Int ?? MatchingAttributeDefaults.intDefaultValue + let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool + + self.init(min: min, max: max, value: value, fallback: fallback) + } + + init(fallback: Bool?) { + self.init( + min: MatchingAttributeDefaults.intDefaultValue, + max: MatchingAttributeDefaults.intDefaultMaxValue, + value: MatchingAttributeDefaults.intDefaultValue, + fallback: fallback + ) + } +} diff --git a/Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift b/Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift new file mode 100644 index 000000000..42fbb0d99 --- /dev/null +++ b/Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift @@ -0,0 +1,42 @@ +// +// SingleValueMatchingAttribute.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 protocol SingleValueMatchingAttribute: MatchingAttribute, Equatable { + associatedtype Value: Equatable + + var value: Value? { get set } + var fallback: Bool? { get set } + + init(value: Value?, fallback: Bool?) +} + +public extension SingleValueMatchingAttribute { + + init(jsonMatchingAttribute: AnyDecodable) { + guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { + self.init(value: nil, fallback: nil) + return + } + + let value = jsonMatchingAttribute[RuleAttributes.value] as? Value + let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool + self.init(value: value, fallback: fallback) + } +} From 3db9c9d05c87feaff27da50ffb1a3018c0a63d81 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 21:37:25 +0200 Subject: [PATCH 02/12] Add unit tests for IsInstalledMacAppStore --- .../DesktopUserAttributeMatcherTests.swift | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift new file mode 100644 index 000000000..65a184f10 --- /dev/null +++ b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift @@ -0,0 +1,99 @@ +// +// DesktopUserAttributeMatcherTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKit +import BrowserServicesKitTestsUtils +import Foundation +import RemoteMessagingTestsUtils +import XCTest +@testable import RemoteMessaging + +class DesktopUserAttributeMatcherTests: XCTestCase { + + var mockStatisticsStore: MockStatisticsStore! + var manager: MockVariantManager! + var emailManager: EmailManager! + var matcher: DesktopUserAttributeMatcher! + var dateYesterday: Date! + + override func setUpWithError() throws { + let now = Calendar.current.dateComponents(in: .current, from: Date()) + let yesterday = DateComponents(year: now.year, month: now.month, day: now.day! - 1) + let dateYesterday = Calendar.current.date(from: yesterday)! + + mockStatisticsStore = MockStatisticsStore() + mockStatisticsStore.atb = "v105-2" + mockStatisticsStore.appRetentionAtb = "v105-44" + mockStatisticsStore.searchRetentionAtb = "v105-88" + mockStatisticsStore.installDate = dateYesterday + + manager = MockVariantManager(isSupportedReturns: true, + currentVariant: MockVariant(name: "zo", weight: 44, isIncluded: { return true }, features: [.dummy])) + let emailManagerStorage = MockEmailManagerStorage() + + // EmailEnabledMatchingAttribute isSignedIn = true + emailManagerStorage.mockUsername = "username" + emailManagerStorage.mockToken = "token" + + emailManager = EmailManager(storage: emailManagerStorage) + setUpUserAttributeMatcher() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + + matcher = nil + } + + // MARK: - InstalledMacAppStore + + func testWhenInstalledMacAppStoreMatchesThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: IsInstalledMacAppStoreMatchingAttribute(value: false, fallback: nil)), + .match) + } + + func testWhenWidgetAddedDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: IsInstalledMacAppStoreMatchingAttribute(value: true, fallback: nil)), + .fail) + } + + // MARK: - + + private func setUpUserAttributeMatcher(dismissedMessageIds: [String] = []) { + matcher = DesktopUserAttributeMatcher( + statisticsStore: mockStatisticsStore, + variantManager: manager, + emailManager: emailManager, + bookmarksCount: 44, + favoritesCount: 88, + appTheme: "default", + daysSinceNetPEnabled: 3, + isPrivacyProEligibleUser: true, + isPrivacyProSubscriber: true, + privacyProDaysSinceSubscribed: 5, + privacyProDaysUntilExpiry: 25, + privacyProPurchasePlatform: "apple", + isPrivacyProSubscriptionActive: true, + isPrivacyProSubscriptionExpiring: false, + isPrivacyProSubscriptionExpired: false, + dismissedMessageIds: dismissedMessageIds, + isInstalledMacAppStore: false + ) + } +} From f77c20be9b31d475e8398d542f73935312a55dda Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 22:25:33 +0200 Subject: [PATCH 03/12] Add StringRangeMatchingAttribute --- .../Model/MatchingAttributes.swift | 98 ++----------------- .../Model/StringRangeMatchingAttribute.swift | 57 +++++++++++ 2 files changed, 63 insertions(+), 92 deletions(-) create mode 100644 Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 583fcdd8a..2b12fb7b2 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -41,42 +41,13 @@ struct LocaleMatchingAttribute: SingleValueMatchingAttribute { } } -struct OSMatchingAttribute: MatchingAttribute, Equatable { +struct OSMatchingAttribute: StringRangeMatchingAttribute { + static let defaultMaxValue: String = AppVersion.shared.osVersion + var min: String = MatchingAttributeDefaults.stringDefaultValue var max: String = AppVersion.shared.osVersion var value: String = MatchingAttributeDefaults.stringDefaultValue var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? String { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? String { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: String = MatchingAttributeDefaults.stringDefaultValue, - max: String = AppVersion.shared.osVersion, - value: String = MatchingAttributeDefaults.stringDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: OSMatchingAttribute, rhs: OSMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } struct IsInternalUserMatchingAttribute: SingleValueMatchingAttribute { @@ -89,42 +60,13 @@ struct AppIdMatchingAttribute: SingleValueMatchingAttribute { var fallback: Bool? } -struct AppVersionMatchingAttribute: MatchingAttribute, Equatable { +struct AppVersionMatchingAttribute: StringRangeMatchingAttribute { + static let defaultMaxValue: String = AppVersion.shared.versionAndBuildNumber + var min: String = MatchingAttributeDefaults.stringDefaultValue var max: String = AppVersion.shared.versionAndBuildNumber var value: String = MatchingAttributeDefaults.stringDefaultValue var fallback: Bool? - - init(jsonMatchingAttribute: AnyDecodable) { - guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } - - if let min = jsonMatchingAttribute[RuleAttributes.min] as? String { - self.min = min - } - if let max = jsonMatchingAttribute[RuleAttributes.max] as? String { - self.max = max - } - if let value = jsonMatchingAttribute[RuleAttributes.value] as? String { - self.value = value - } - if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { - self.fallback = fallback - } - } - - init(min: String = MatchingAttributeDefaults.stringDefaultValue, - max: String = AppVersion.shared.versionAndBuildNumber, - value: String = MatchingAttributeDefaults.stringDefaultValue, - fallback: Bool?) { - self.min = min - self.max = max - self.value = value - self.fallback = fallback - } - - static func == (lhs: AppVersionMatchingAttribute, rhs: AppVersionMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback - } } struct AtbMatchingAttribute: SingleValueMatchingAttribute { @@ -248,10 +190,6 @@ struct UnknownMatchingAttribute: MatchingAttribute, Equatable { init(fallback: Bool?) { self.fallback = fallback } - - static func == (lhs: UnknownMatchingAttribute, rhs: UnknownMatchingAttribute) -> Bool { - return lhs.fallback == rhs.fallback - } } enum MatchingAttributeDefaults { @@ -272,10 +210,6 @@ struct BooleanMatchingAttribute: Equatable { func matches(value: Bool) -> EvaluationResult { return EvaluationResultModel.result(value: self.value == value) } - - static func == (lhs: BooleanMatchingAttribute, rhs: BooleanMatchingAttribute) -> Bool { - return lhs.value == rhs.value - } } struct IntMatchingAttribute: Equatable { @@ -288,10 +222,6 @@ struct IntMatchingAttribute: Equatable { func matches(value: Int) -> EvaluationResult { return EvaluationResultModel.result(value: self.value == value) } - - static func == (lhs: IntMatchingAttribute, rhs: IntMatchingAttribute) -> Bool { - return lhs.value == rhs.value - } } struct RangeIntMatchingAttribute: Equatable { @@ -301,10 +231,6 @@ struct RangeIntMatchingAttribute: Equatable { func matches(value: Int) -> EvaluationResult { return EvaluationResultModel.result(value: (value >= self.min) && (value <= self.max)) } - - static func == (lhs: RangeIntMatchingAttribute, rhs: RangeIntMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max - } } struct StringMatchingAttribute: Equatable { @@ -317,10 +243,6 @@ struct StringMatchingAttribute: Equatable { func matches(value: String) -> EvaluationResult { return EvaluationResultModel.result(value: self.value == value.lowercased()) } - - static func == (lhs: StringMatchingAttribute, rhs: StringMatchingAttribute) -> Bool { - return lhs.value == rhs.value - } } struct StringArrayMatchingAttribute: Equatable { @@ -333,10 +255,6 @@ struct StringArrayMatchingAttribute: Equatable { func matches(value: String) -> EvaluationResult { return EvaluationResultModel.result(value: values.contains(value.lowercased())) } - - static func == (lhs: StringArrayMatchingAttribute, rhs: StringArrayMatchingAttribute) -> Bool { - return lhs.values == rhs.values - } } struct RangeStringNumericMatchingAttribute: Equatable { @@ -368,8 +286,4 @@ struct RangeStringNumericMatchingAttribute: Equatable { return version + String(repeating: ".0", count: matchComponents.count - versionComponents.count) } - - static func == (lhs: RangeStringNumericMatchingAttribute, rhs: RangeStringNumericMatchingAttribute) -> Bool { - return lhs.min == rhs.min && lhs.max == rhs.max - } } diff --git a/Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift b/Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift new file mode 100644 index 000000000..95080a4ac --- /dev/null +++ b/Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift @@ -0,0 +1,57 @@ +// +// StringRangeMatchingAttribute.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol StringRangeMatchingAttribute: MatchingAttribute, Equatable { + var min: String { get set } + var max: String { get set } + var value: String { get set } + var fallback: Bool? { get set } + + static var defaultMaxValue: String { get } + + init(min: String, max: String, value: String, fallback: Bool?) +} + +extension StringRangeMatchingAttribute { + + init(jsonMatchingAttribute: AnyDecodable) { + guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { + self.init(fallback: nil) + return + } + + let min = jsonMatchingAttribute[RuleAttributes.min] as? String ?? MatchingAttributeDefaults.stringDefaultValue + let max = jsonMatchingAttribute[RuleAttributes.max] as? String ?? Self.defaultMaxValue + let value = jsonMatchingAttribute[RuleAttributes.value] as? String ?? MatchingAttributeDefaults.stringDefaultValue + let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool + + self.init(min: min, max: max, value: value, fallback: fallback) + } + + init(fallback: Bool?) { + self.init( + min: MatchingAttributeDefaults.stringDefaultValue, + max: Self.defaultMaxValue, + value: MatchingAttributeDefaults.stringDefaultValue, + fallback: fallback + ) + } +} From 4237c2f4689c1aa95d5672797f130ff028d0bb75 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 22:37:07 +0200 Subject: [PATCH 04/12] Move MatchingAttributes protocols into a separate folder --- .../Model/MatchingAttributes.swift | 66 +++++++------------ .../MatchingAttribute.swift | 37 +++++++++++ .../NumericRangeMatching.swift} | 6 +- .../SingleValueMatching.swift} | 6 +- .../StringRangeMatching.swift} | 7 +- 5 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/MatchingAttribute.swift rename Sources/RemoteMessaging/Model/{NumericRangeMatchingAttribute.swift => MatchingAttributesPrototypes/NumericRangeMatching.swift} (91%) rename Sources/RemoteMessaging/Model/{SingleValueMatchingAttribute.swift => MatchingAttributesPrototypes/SingleValueMatching.swift} (88%) rename Sources/RemoteMessaging/Model/{StringRangeMatchingAttribute.swift => MatchingAttributesPrototypes/StringRangeMatching.swift} (91%) diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 2b12fb7b2..0a570c0f5 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -19,19 +19,7 @@ import Foundation import Common -enum RuleAttributes { - static let min = "min" - static let max = "max" - static let value = "value" - static let fallback = "fallback" - static let since = "since" -} - -public protocol MatchingAttribute { - var fallback: Bool? { get } -} - -struct LocaleMatchingAttribute: SingleValueMatchingAttribute { +struct LocaleMatchingAttribute: SingleValueMatching { var value: [String]? = [] var fallback: Bool? @@ -41,7 +29,7 @@ struct LocaleMatchingAttribute: SingleValueMatchingAttribute { } } -struct OSMatchingAttribute: StringRangeMatchingAttribute { +struct OSMatchingAttribute: StringRangeMatching { static let defaultMaxValue: String = AppVersion.shared.osVersion var min: String = MatchingAttributeDefaults.stringDefaultValue @@ -50,17 +38,17 @@ struct OSMatchingAttribute: StringRangeMatchingAttribute { var fallback: Bool? } -struct IsInternalUserMatchingAttribute: SingleValueMatchingAttribute { +struct IsInternalUserMatchingAttribute: SingleValueMatching { var value: Bool? var fallback: Bool? } -struct AppIdMatchingAttribute: SingleValueMatchingAttribute { +struct AppIdMatchingAttribute: SingleValueMatching { var value: String? var fallback: Bool? } -struct AppVersionMatchingAttribute: StringRangeMatchingAttribute { +struct AppVersionMatchingAttribute: StringRangeMatching { static let defaultMaxValue: String = AppVersion.shared.versionAndBuildNumber var min: String = MatchingAttributeDefaults.stringDefaultValue @@ -69,109 +57,109 @@ struct AppVersionMatchingAttribute: StringRangeMatchingAttribute { var fallback: Bool? } -struct AtbMatchingAttribute: SingleValueMatchingAttribute { +struct AtbMatchingAttribute: SingleValueMatching { var value: String? var fallback: Bool? } -struct AppAtbMatchingAttribute: SingleValueMatchingAttribute { +struct AppAtbMatchingAttribute: SingleValueMatching { var value: String? var fallback: Bool? } -struct SearchAtbMatchingAttribute: SingleValueMatchingAttribute { +struct SearchAtbMatchingAttribute: SingleValueMatching { var value: String? var fallback: Bool? } -struct ExpVariantMatchingAttribute: SingleValueMatchingAttribute { +struct ExpVariantMatchingAttribute: SingleValueMatching { var value: String? var fallback: Bool? } -struct EmailEnabledMatchingAttribute: SingleValueMatchingAttribute { +struct EmailEnabledMatchingAttribute: SingleValueMatching { var value: Bool? var fallback: Bool? } -struct WidgetAddedMatchingAttribute: SingleValueMatchingAttribute { +struct WidgetAddedMatchingAttribute: SingleValueMatching { var value: Bool? var fallback: Bool? } -struct BookmarksMatchingAttribute: NumericRangeMatchingAttribute { +struct BookmarksMatchingAttribute: NumericRangeMatching { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? } -struct FavoritesMatchingAttribute: NumericRangeMatchingAttribute { +struct FavoritesMatchingAttribute: NumericRangeMatching { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? } -struct AppThemeMatchingAttribute: SingleValueMatchingAttribute { +struct AppThemeMatchingAttribute: SingleValueMatching { var value: String? var fallback: Bool? } -struct DaysSinceInstalledMatchingAttribute: NumericRangeMatchingAttribute { +struct DaysSinceInstalledMatchingAttribute: NumericRangeMatching { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? } -struct DaysSinceNetPEnabledMatchingAttribute: NumericRangeMatchingAttribute { +struct DaysSinceNetPEnabledMatchingAttribute: NumericRangeMatching { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? } -struct IsPrivacyProEligibleUserMatchingAttribute: SingleValueMatchingAttribute { +struct IsPrivacyProEligibleUserMatchingAttribute: SingleValueMatching { var value: Bool? var fallback: Bool? } -struct IsPrivacyProSubscriberUserMatchingAttribute: SingleValueMatchingAttribute { +struct IsPrivacyProSubscriberUserMatchingAttribute: SingleValueMatching { var value: Bool? var fallback: Bool? } -struct PrivacyProDaysSinceSubscribedMatchingAttribute: NumericRangeMatchingAttribute { +struct PrivacyProDaysSinceSubscribedMatchingAttribute: NumericRangeMatching { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? } -struct PrivacyProDaysUntilExpiryMatchingAttribute: NumericRangeMatchingAttribute { +struct PrivacyProDaysUntilExpiryMatchingAttribute: NumericRangeMatching { var min: Int = MatchingAttributeDefaults.intDefaultValue var max: Int = MatchingAttributeDefaults.intDefaultMaxValue var value: Int = MatchingAttributeDefaults.intDefaultValue var fallback: Bool? } -struct PrivacyProPurchasePlatformMatchingAttribute: SingleValueMatchingAttribute { +struct PrivacyProPurchasePlatformMatchingAttribute: SingleValueMatching { var value: [String]? = [] var fallback: Bool? } -struct PrivacyProSubscriptionStatusMatchingAttribute: SingleValueMatchingAttribute { +struct PrivacyProSubscriptionStatusMatchingAttribute: SingleValueMatching { var value: [String]? = [] var fallback: Bool? } -struct InteractedWithMessageMatchingAttribute: SingleValueMatchingAttribute { +struct InteractedWithMessageMatchingAttribute: SingleValueMatching { var value: [String]? = [] var fallback: Bool? } -struct IsInstalledMacAppStoreMatchingAttribute: SingleValueMatchingAttribute { +struct IsInstalledMacAppStoreMatchingAttribute: SingleValueMatching { var value: Bool? var fallback: Bool? } @@ -192,12 +180,6 @@ struct UnknownMatchingAttribute: MatchingAttribute, Equatable { } } -enum MatchingAttributeDefaults { - static let intDefaultValue = -1 - static let intDefaultMaxValue = Int.max - static let stringDefaultValue = "" -} - // MARK: - struct BooleanMatchingAttribute: Equatable { diff --git a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/MatchingAttribute.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/MatchingAttribute.swift new file mode 100644 index 000000000..934b9685d --- /dev/null +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/MatchingAttribute.swift @@ -0,0 +1,37 @@ +// +// MatchingAttribute.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 + +enum RuleAttributes { + static let min = "min" + static let max = "max" + static let value = "value" + static let fallback = "fallback" + static let since = "since" +} + +enum MatchingAttributeDefaults { + static let intDefaultValue = -1 + static let intDefaultMaxValue = Int.max + static let stringDefaultValue = "" +} + +public protocol MatchingAttribute { + var fallback: Bool? { get } +} diff --git a/Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift similarity index 91% rename from Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift rename to Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift index 0de0a9e48..a780931b6 100644 --- a/Sources/RemoteMessaging/Model/NumericRangeMatchingAttribute.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift @@ -1,5 +1,5 @@ // -// NumericRangeMatchingAttribute.swift +// NumericRangeMatching.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -18,7 +18,7 @@ import Foundation -public protocol NumericRangeMatchingAttribute: MatchingAttribute, Equatable { +public protocol NumericRangeMatching: MatchingAttribute, Equatable { var min: Int { get set } var max: Int { get set } var value: Int { get set } @@ -27,7 +27,7 @@ public protocol NumericRangeMatchingAttribute: MatchingAttribute, Equatable { init(min: Int, max: Int, value: Int, fallback: Bool?) } -public extension NumericRangeMatchingAttribute { +public extension NumericRangeMatching { init(jsonMatchingAttribute: AnyDecodable) { guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { diff --git a/Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift similarity index 88% rename from Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift rename to Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift index 42fbb0d99..902d8e5ed 100644 --- a/Sources/RemoteMessaging/Model/SingleValueMatchingAttribute.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift @@ -1,5 +1,5 @@ // -// SingleValueMatchingAttribute.swift +// SingleValueMatching.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -18,7 +18,7 @@ import Foundation -public protocol SingleValueMatchingAttribute: MatchingAttribute, Equatable { +public protocol SingleValueMatching: MatchingAttribute, Equatable { associatedtype Value: Equatable var value: Value? { get set } @@ -27,7 +27,7 @@ public protocol SingleValueMatchingAttribute: MatchingAttribute, Equatable { init(value: Value?, fallback: Bool?) } -public extension SingleValueMatchingAttribute { +public extension SingleValueMatching { init(jsonMatchingAttribute: AnyDecodable) { guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { diff --git a/Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift similarity index 91% rename from Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift rename to Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift index 95080a4ac..c2091402b 100644 --- a/Sources/RemoteMessaging/Model/StringRangeMatchingAttribute.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift @@ -1,6 +1,5 @@ // -// StringRangeMatchingAttribute.swift -// DuckDuckGo +// StringRangeMatching.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,7 +18,7 @@ import Foundation -protocol StringRangeMatchingAttribute: MatchingAttribute, Equatable { +protocol StringRangeMatching: MatchingAttribute, Equatable { var min: String { get set } var max: String { get set } var value: String { get set } @@ -30,7 +29,7 @@ protocol StringRangeMatchingAttribute: MatchingAttribute, Equatable { init(min: String, max: String, value: String, fallback: Bool?) } -extension StringRangeMatchingAttribute { +extension StringRangeMatching { init(jsonMatchingAttribute: AnyDecodable) { guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { From 0ee0ec75af36f0e4d986a268ecf815c9ccb0f45a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 23:03:15 +0200 Subject: [PATCH 05/12] Move IsInstalledMacAppStore to AppAttributeMatcher, add PinnedTabsAttributeMatcher --- .../JsonToRemoteMessageModelMapper.swift | 2 + .../Matchers/AppAttributeMatcher.swift | 54 ++++++++++++++++- .../Matchers/UserAttributeMatcher.swift | 17 +++--- .../Model/MatchingAttributes.swift | 7 +++ ...t => CommonAppAttributeMatcherTests.swift} | 9 ++- .../DesktopAppAttributeMatcherTests.swift | 60 +++++++++++++++++++ .../DesktopUserAttributeMatcherTests.swift | 37 ++++++++++-- .../RemoteMessagingConfigMatcherTests.swift | 16 ++--- .../RemoteMessagingConfigProcessorTests.swift | 4 +- .../RemoteMessagingProcessingTests.swift | 2 +- .../RemoteMessagingStoreTests.swift | 2 +- 11 files changed, 177 insertions(+), 33 deletions(-) rename Tests/RemoteMessagingTests/Matchers/{AppAttributeMatcherTests.swift => CommonAppAttributeMatcherTests.swift} (96%) create mode 100644 Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index b85ba5cbf..33d5e0903 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -44,6 +44,7 @@ private enum AttributesKey: String, CaseIterable { case pproSubscriptionStatus case interactedWithMessage case isInstalledMacAppStore + case pinnedTabs func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { switch self { @@ -71,6 +72,7 @@ private enum AttributesKey: String, CaseIterable { case .pproSubscriptionStatus: return PrivacyProSubscriptionStatusMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .interactedWithMessage: return InteractedWithMessageMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .isInstalledMacAppStore: return IsInstalledMacAppStoreMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .pinnedTabs: return PinnedTabsMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } } diff --git a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift index 039849026..b3212e9b2 100644 --- a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift @@ -20,7 +20,59 @@ import Foundation import Common import BrowserServicesKit -public struct AppAttributeMatcher: AttributeMatching { +#if os(iOS) +public typealias AppAttributeMatcher = MobileAppAttributeMatcher +#elseif os(macOS) +public typealias AppAttributeMatcher = DesktopAppAttributeMatcher +#endif + +public typealias MobileAppAttributeMatcher = CommonAppAttributeMatcher + +public struct DesktopAppAttributeMatcher: AttributeMatching { + private let isInstalledMacAppStore: Bool + + private let commonAppAttributeMatcher: CommonAppAttributeMatcher + + public init(statisticsStore: StatisticsStore, variantManager: VariantManager, isInternalUser: Bool = true, isInstalledMacAppStore: Bool) { + self.isInstalledMacAppStore = isInstalledMacAppStore + + commonAppAttributeMatcher = .init(statisticsStore: statisticsStore, variantManager: variantManager, isInternalUser: isInternalUser) + } + + public init( + bundleId: String, + appVersion: String, + isInternalUser: Bool, + statisticsStore: StatisticsStore, + variantManager: VariantManager, + isInstalledMacAppStore: Bool + ) { + self.isInstalledMacAppStore = isInternalUser + + commonAppAttributeMatcher = .init( + bundleId: bundleId, + appVersion: appVersion, + isInternalUser: isInternalUser, + statisticsStore: statisticsStore, + variantManager: variantManager + ) + } + + public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { + switch matchingAttribute { + case let matchingAttribute as IsInstalledMacAppStoreMatchingAttribute: + guard let value = matchingAttribute.value else { + return .fail + } + + return BooleanMatchingAttribute(value).matches(value: isInstalledMacAppStore) + default: + return commonAppAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) + } + } +} + +public struct CommonAppAttributeMatcher: AttributeMatching { private let bundleId: String private let appVersion: String diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 6cd8ec2e1..476e95aa1 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -94,7 +94,7 @@ public struct MobileUserAttributeMatcher: AttributeMatching { } public struct DesktopUserAttributeMatcher: AttributeMatching { - private let isInstalledMacAppStore: Bool + private let pinnedTabsCount: Int private let commonUserAttributeMatcher: CommonUserAttributeMatcher @@ -114,9 +114,9 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { isPrivacyProSubscriptionExpiring: Bool, isPrivacyProSubscriptionExpired: Bool, dismissedMessageIds: [String], - isInstalledMacAppStore: Bool + pinnedTabsCount: Int ) { - self.isInstalledMacAppStore = isInstalledMacAppStore + self.pinnedTabsCount = pinnedTabsCount commonUserAttributeMatcher = .init( statisticsStore: statisticsStore, @@ -140,19 +140,18 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { - case let matchingAttribute as IsInstalledMacAppStoreMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail + case let matchingAttribute as PinnedTabsMatchingAttribute: + if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { + return IntMatchingAttribute(matchingAttribute.value).matches(value: pinnedTabsCount) + } else { + return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: pinnedTabsCount) } - - return BooleanMatchingAttribute(value).matches(value: isInstalledMacAppStore) default: return commonUserAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) } } } - public struct CommonUserAttributeMatcher: AttributeMatching { private enum PrivacyProSubscriptionStatus: String { diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 0a570c0f5..7accf2cec 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -164,6 +164,13 @@ struct IsInstalledMacAppStoreMatchingAttribute: SingleValueMatching { var fallback: Bool? } +struct PinnedTabsMatchingAttribute: NumericRangeMatching { + var min: Int = MatchingAttributeDefaults.intDefaultValue + var max: Int = MatchingAttributeDefaults.intDefaultMaxValue + var value: Int = MatchingAttributeDefaults.intDefaultValue + var fallback: Bool? +} + struct UnknownMatchingAttribute: MatchingAttribute, Equatable { var fallback: Bool? diff --git a/Tests/RemoteMessagingTests/Matchers/AppAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/CommonAppAttributeMatcherTests.swift similarity index 96% rename from Tests/RemoteMessagingTests/Matchers/AppAttributeMatcherTests.swift rename to Tests/RemoteMessagingTests/Matchers/CommonAppAttributeMatcherTests.swift index 9ca1b09d7..7c9d87672 100644 --- a/Tests/RemoteMessagingTests/Matchers/AppAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/CommonAppAttributeMatcherTests.swift @@ -1,5 +1,5 @@ // -// AppAttributeMatcherTests.swift +// CommonAppAttributeMatcherTests.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -23,9 +23,9 @@ import RemoteMessagingTestsUtils import XCTest @testable import RemoteMessaging -class AppAttributeMatcherTests: XCTestCase { +class CommonAppAttributeMatcherTests: XCTestCase { - private var matcher: AppAttributeMatcher! + private var matcher: CommonAppAttributeMatcher! override func setUpWithError() throws { try super.setUpWithError() @@ -36,8 +36,7 @@ class AppAttributeMatcherTests: XCTestCase { mockStatisticsStore.searchRetentionAtb = "v105-88" let manager = MockVariantManager(isSupportedReturns: true, currentVariant: MockVariant(name: "zo", weight: 44, isIncluded: { return true }, features: [.dummy])) - matcher = AppAttributeMatcher(statisticsStore: mockStatisticsStore, - variantManager: manager) + matcher = CommonAppAttributeMatcher(statisticsStore: mockStatisticsStore, variantManager: manager) } override func tearDownWithError() throws { diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift new file mode 100644 index 000000000..66e6858af --- /dev/null +++ b/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift @@ -0,0 +1,60 @@ +// +// DesktopAppAttributeMatcherTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKitTestsUtils +import Common +import Foundation +import RemoteMessagingTestsUtils +import XCTest +@testable import RemoteMessaging + +class DesktopAppAttributeMatcherTests: XCTestCase { + + private var matcher: DesktopAppAttributeMatcher! + + override func setUpWithError() throws { + try super.setUpWithError() + + let mockStatisticsStore = MockStatisticsStore() + mockStatisticsStore.atb = "v105-2" + mockStatisticsStore.appRetentionAtb = "v105-44" + mockStatisticsStore.searchRetentionAtb = "v105-88" + + let manager = MockVariantManager(isSupportedReturns: true, currentVariant: MockVariant(name: "zo", weight: 44, isIncluded: { return true }, features: [.dummy])) + matcher = DesktopAppAttributeMatcher(statisticsStore: mockStatisticsStore, variantManager: manager, isInstalledMacAppStore: false) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + + matcher = nil + } + + // MARK: - InstalledMacAppStore + + func testWhenInstalledMacAppStoreMatchesThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: IsInstalledMacAppStoreMatchingAttribute(value: false, fallback: nil)), + .match) + } + + func testWhenWidgetAddedDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: IsInstalledMacAppStoreMatchingAttribute(value: true, fallback: nil)), + .fail) + } +} diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift index 65a184f10..fab6225a2 100644 --- a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift @@ -61,15 +61,40 @@ class DesktopUserAttributeMatcherTests: XCTestCase { matcher = nil } - // MARK: - InstalledMacAppStore + // MARK: - PinnedTabs - func testWhenInstalledMacAppStoreMatchesThenReturnMatch() throws { - XCTAssertEqual(matcher.evaluate(matchingAttribute: IsInstalledMacAppStoreMatchingAttribute(value: false, fallback: nil)), + func testWhenPinnedTabsMatchesThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(value: 3, fallback: nil)), .match) } - func testWhenWidgetAddedDoesNotMatchThenReturnFail() throws { - XCTAssertEqual(matcher.evaluate(matchingAttribute: IsInstalledMacAppStoreMatchingAttribute(value: true, fallback: nil)), + func testWhenPinnedTabsDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(value: 2, fallback: nil)), + .fail) + } + + func testWhenPinnedTabsEqualOrLowerThanMaxThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(max: 4, fallback: nil)), + .match) + } + + func testWhenPinnedTabsGreaterThanMaxThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(max: 0, fallback: nil)), + .fail) + } + + func testWhenPinnedTabsLowerThanMinThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(min: 6, fallback: nil)), + .fail) + } + + func testWhenPinnedTabsInRangeThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(min: 2, max: 18, fallback: nil)), + .match) + } + + func testWhenPinnedTabsNotInRangeThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: PinnedTabsMatchingAttribute(min: 9, max: 11, fallback: nil)), .fail) } @@ -93,7 +118,7 @@ class DesktopUserAttributeMatcherTests: XCTestCase { isPrivacyProSubscriptionExpiring: false, isPrivacyProSubscriptionExpired: false, dismissedMessageIds: dismissedMessageIds, - isInstalledMacAppStore: false + pinnedTabsCount: 3 ) } } diff --git a/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift b/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift index 980f9160b..a892ab422 100644 --- a/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift @@ -36,7 +36,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { let emailManager = EmailManager(storage: emailManagerStorage) matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager(), @@ -131,7 +131,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { func testWhenMatchingMessageShouldBeExcludedThenReturnNull() { matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), deviceAttributeMatcher: DeviceAttributeMatcher(osVersion: AppVersion.shared.osVersion, locale: "en-US"), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), @@ -237,7 +237,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { func testWhenUserDismissedMessagesAndDeviceMatchesMultipleMessagesThenReturnFirstMatchNotDismissed() { matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager(), @@ -284,7 +284,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { func testWhenDeviceDoesNotMatchAnyRuleThenReturnNull() { let os = ProcessInfo().operatingSystemVersion matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), deviceAttributeMatcher: DeviceAttributeMatcher(osVersion: AppVersion.shared.osVersion, locale: "en-US"), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), @@ -328,7 +328,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { percentileStore.defaultPercentage = 0.1 matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), deviceAttributeMatcher: DeviceAttributeMatcher(osVersion: AppVersion.shared.osVersion, locale: "en-US"), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), @@ -370,7 +370,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { percentileStore.defaultPercentage = 0.5 matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), deviceAttributeMatcher: DeviceAttributeMatcher(osVersion: AppVersion.shared.osVersion, locale: "en-US"), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), @@ -412,7 +412,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { percentileStore.defaultPercentage = 0.3 matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), deviceAttributeMatcher: DeviceAttributeMatcher(osVersion: AppVersion.shared.osVersion, locale: "en-US"), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), @@ -454,7 +454,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { percentileStore.defaultPercentage = 0.6 matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), deviceAttributeMatcher: DeviceAttributeMatcher(osVersion: AppVersion.shared.osVersion, locale: "en-US"), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), diff --git a/Tests/RemoteMessagingTests/RemoteMessagingConfigProcessorTests.swift b/Tests/RemoteMessagingTests/RemoteMessagingConfigProcessorTests.swift index 9f9b33ad8..76222d589 100644 --- a/Tests/RemoteMessagingTests/RemoteMessagingConfigProcessorTests.swift +++ b/Tests/RemoteMessagingTests/RemoteMessagingConfigProcessorTests.swift @@ -28,7 +28,7 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { XCTAssertNotNil(jsonRemoteMessagingConfig) let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager(), @@ -68,7 +68,7 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { XCTAssertNotNil(jsonRemoteMessagingConfig) let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager(), diff --git a/Tests/RemoteMessagingTests/RemoteMessagingProcessingTests.swift b/Tests/RemoteMessagingTests/RemoteMessagingProcessingTests.swift index 5099cc497..17e1e13c8 100644 --- a/Tests/RemoteMessagingTests/RemoteMessagingProcessingTests.swift +++ b/Tests/RemoteMessagingTests/RemoteMessagingProcessingTests.swift @@ -63,7 +63,7 @@ class RemoteMessagingProcessingTests: XCTestCase { availabilityProvider = MockRemoteMessagingAvailabilityProvider() let matcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager(), diff --git a/Tests/RemoteMessagingTests/RemoteMessagingStoreTests.swift b/Tests/RemoteMessagingTests/RemoteMessagingStoreTests.swift index fb2e65fae..ffede3d3a 100644 --- a/Tests/RemoteMessagingTests/RemoteMessagingStoreTests.swift +++ b/Tests/RemoteMessagingTests/RemoteMessagingStoreTests.swift @@ -277,7 +277,7 @@ class RemoteMessagingStoreTests: XCTestCase { func processorResult() throws -> RemoteMessagingConfigProcessor.ProcessorResult { let jsonRemoteMessagingConfig = try decodeJson(fileName: "remote-messaging-config-example.json") let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), + appAttributeMatcher: MobileAppAttributeMatcher(statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager()), userAttributeMatcher: MobileUserAttributeMatcher( statisticsStore: MockStatisticsStore(), variantManager: MockVariantManager(), From e4f80ed0bbe6b0f13f47469ab2b23c7d86876aaa Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Jul 2024 23:15:52 +0200 Subject: [PATCH 06/12] Add CustomHomePageMatchingAttribute --- .../Mappers/JsonToRemoteMessageModelMapper.swift | 6 ++++-- .../Matchers/UserAttributeMatcher.swift | 10 +++++++++- .../Model/MatchingAttributes.swift | 5 +++++ .../DesktopUserAttributeMatcherTests.swift | 15 ++++++++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index 33d5e0903..2d172cc7c 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -43,8 +43,9 @@ private enum AttributesKey: String, CaseIterable { case pproPurchasePlatform case pproSubscriptionStatus case interactedWithMessage - case isInstalledMacAppStore + case installedMacAppStore case pinnedTabs + case customHomePage func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { switch self { @@ -71,8 +72,9 @@ private enum AttributesKey: String, CaseIterable { case .pproPurchasePlatform: return PrivacyProPurchasePlatformMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .pproSubscriptionStatus: return PrivacyProSubscriptionStatusMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .interactedWithMessage: return InteractedWithMessageMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) - case .isInstalledMacAppStore: return IsInstalledMacAppStoreMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .installedMacAppStore: return IsInstalledMacAppStoreMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .pinnedTabs: return PinnedTabsMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .customHomePage: return CustomHomePageMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } } diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 476e95aa1..9b83d4080 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -95,6 +95,7 @@ public struct MobileUserAttributeMatcher: AttributeMatching { public struct DesktopUserAttributeMatcher: AttributeMatching { private let pinnedTabsCount: Int + private let hasCustomHomePage: Bool private let commonUserAttributeMatcher: CommonUserAttributeMatcher @@ -114,9 +115,11 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { isPrivacyProSubscriptionExpiring: Bool, isPrivacyProSubscriptionExpired: Bool, dismissedMessageIds: [String], - pinnedTabsCount: Int + pinnedTabsCount: Int, + hasCustomHomePage: Bool ) { self.pinnedTabsCount = pinnedTabsCount + self.hasCustomHomePage = hasCustomHomePage commonUserAttributeMatcher = .init( statisticsStore: statisticsStore, @@ -146,6 +149,11 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { } else { return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: pinnedTabsCount) } + case let matchingAttribute as CustomHomePageMatchingAttribute: + guard let value = matchingAttribute.value else { + return .fail + } + return BooleanMatchingAttribute(value).matches(value: hasCustomHomePage) default: return commonUserAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) } diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 7accf2cec..491caa534 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -171,6 +171,11 @@ struct PinnedTabsMatchingAttribute: NumericRangeMatching { var fallback: Bool? } +struct CustomHomePageMatchingAttribute: SingleValueMatching { + var value: Bool? + var fallback: Bool? +} + struct UnknownMatchingAttribute: MatchingAttribute, Equatable { var fallback: Bool? diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift index fab6225a2..6945387c9 100644 --- a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift @@ -98,6 +98,18 @@ class DesktopUserAttributeMatcherTests: XCTestCase { .fail) } + // MARK: - CustomHomePage + + func testWhenCustomHomePageMatchesThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: CustomHomePageMatchingAttribute(value: true, fallback: nil)), + .match) + } + + func testWhenCustomHomePageDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: CustomHomePageMatchingAttribute(value: false, fallback: nil)), + .fail) + } + // MARK: - private func setUpUserAttributeMatcher(dismissedMessageIds: [String] = []) { @@ -118,7 +130,8 @@ class DesktopUserAttributeMatcherTests: XCTestCase { isPrivacyProSubscriptionExpiring: false, isPrivacyProSubscriptionExpired: false, dismissedMessageIds: dismissedMessageIds, - pinnedTabsCount: 3 + pinnedTabsCount: 3, + hasCustomHomePage: true ) } } From 148f0bb4b5400a50e871138c00cfc5da8f01f5cd Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 10 Jul 2024 09:43:33 +0200 Subject: [PATCH 07/12] Fix Swiftlint violations --- .../Matchers/DesktopAppAttributeMatcherTests.swift | 1 - .../Matchers/DesktopUserAttributeMatcherTests.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift index 66e6858af..fc70f7e57 100644 --- a/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/DesktopAppAttributeMatcherTests.swift @@ -1,6 +1,5 @@ // // DesktopAppAttributeMatcherTests.swift -// DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. // diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift index 6945387c9..aa2887834 100644 --- a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift @@ -1,6 +1,5 @@ // // DesktopUserAttributeMatcherTests.swift -// DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. // From d1090e95836ab7f91d98bdb60a40b095c6c248fa Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 12 Jul 2024 10:12:41 +0200 Subject: [PATCH 08/12] Add duckPlayerOnboarded and duckPlayerEnabled and move matching logic to matching protocols --- .../JsonToRemoteMessageModelMapper.swift | 4 + .../Matchers/AppAttributeMatcher.swift | 40 ++------- .../Matchers/DeviceAttributeMatcher.swift | 7 +- .../Matchers/UserAttributeMatcher.swift | 84 ++++++------------- .../Model/MatchingAttributes.swift | 10 +++ .../NumericRangeMatching.swift | 7 ++ .../SingleValueMatching.swift | 34 ++++++++ .../StringRangeMatching.swift | 7 ++ .../DesktopUserAttributeMatcherTests.swift | 28 ++++++- 9 files changed, 124 insertions(+), 97 deletions(-) diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index 2d172cc7c..eb19322db 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -46,6 +46,8 @@ private enum AttributesKey: String, CaseIterable { case installedMacAppStore case pinnedTabs case customHomePage + case duckPlayerOnboarded + case duckPlayerEnabled func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { switch self { @@ -75,6 +77,8 @@ private enum AttributesKey: String, CaseIterable { case .installedMacAppStore: return IsInstalledMacAppStoreMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .pinnedTabs: return PinnedTabsMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .customHomePage: return CustomHomePageMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .duckPlayerOnboarded: return DuckPlayerOnboardedMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .duckPlayerEnabled: return DuckPlayerEnabledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } } diff --git a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift index b3212e9b2..50acd49f8 100644 --- a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift @@ -102,46 +102,22 @@ public struct CommonAppAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as IsInternalUserMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: isInternalUser) + return matchingAttribute.evaluate(for: isInternalUser) case let matchingAttribute as AppIdMatchingAttribute: - guard let value = matchingAttribute.value, !value.isEmpty else { + guard matchingAttribute.value?.isEmpty == false else { return .fail } - - return StringMatchingAttribute(value).matches(value: bundleId) + return matchingAttribute.evaluate(for: bundleId) case let matchingAttribute as AppVersionMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.stringDefaultValue { - return StringMatchingAttribute(matchingAttribute.value).matches(value: appVersion) - } else { - return RangeStringNumericMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: appVersion) - } + return matchingAttribute.evaluate(for: appVersion) case let matchingAttribute as AtbMatchingAttribute: - guard let atb = statisticsStore.atb, let value = matchingAttribute.value else { - return .fail - } - - return StringMatchingAttribute(value).matches(value: atb) + return matchingAttribute.evaluate(for: statisticsStore.atb) case let matchingAttribute as AppAtbMatchingAttribute: - guard let atb = statisticsStore.appRetentionAtb, let value = matchingAttribute.value else { - return .fail - } - - return StringMatchingAttribute(value).matches(value: atb) + return matchingAttribute.evaluate(for: statisticsStore.appRetentionAtb) case let matchingAttribute as SearchAtbMatchingAttribute: - guard let atb = statisticsStore.searchRetentionAtb, let value = matchingAttribute.value else { - return .fail - } - return StringMatchingAttribute(value).matches(value: atb) + return matchingAttribute.evaluate(for: statisticsStore.searchRetentionAtb) case let matchingAttribute as ExpVariantMatchingAttribute: - guard let variant = variantManager.currentVariant?.name, let value = matchingAttribute.value else { - return .fail - } - - return StringMatchingAttribute(value).matches(value: variant) + return matchingAttribute.evaluate(for: variantManager.currentVariant?.name) default: return nil } diff --git a/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift index 252182f0b..f3ffac52e 100644 --- a/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift @@ -36,12 +36,9 @@ public struct DeviceAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as LocaleMatchingAttribute: - return StringArrayMatchingAttribute(matchingAttribute.value).matches(value: LocaleMatchingAttribute.localeIdentifierAsJsonFormat(localeIdentifier)) + return matchingAttribute.evaluate(for: LocaleMatchingAttribute.localeIdentifierAsJsonFormat(localeIdentifier)) case let matchingAttribute as OSMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.stringDefaultValue { - return StringMatchingAttribute(matchingAttribute.value).matches(value: osVersion) - } - return RangeStringNumericMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: osVersion) + return matchingAttribute.evaluate(for: osVersion) default: return nil } diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 9b83d4080..660344c02 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -96,6 +96,8 @@ public struct MobileUserAttributeMatcher: AttributeMatching { public struct DesktopUserAttributeMatcher: AttributeMatching { private let pinnedTabsCount: Int private let hasCustomHomePage: Bool + private let isDuckPlayerOnboarded: Bool + private let isDuckPlayerEnabled: Bool private let commonUserAttributeMatcher: CommonUserAttributeMatcher @@ -116,10 +118,14 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { isPrivacyProSubscriptionExpired: Bool, dismissedMessageIds: [String], pinnedTabsCount: Int, - hasCustomHomePage: Bool + hasCustomHomePage: Bool, + isDuckPlayerOnboarded: Bool, + isDuckPlayerEnabled: Bool ) { self.pinnedTabsCount = pinnedTabsCount self.hasCustomHomePage = hasCustomHomePage + self.isDuckPlayerOnboarded = isDuckPlayerOnboarded + self.isDuckPlayerEnabled = isDuckPlayerEnabled commonUserAttributeMatcher = .init( statisticsStore: statisticsStore, @@ -150,10 +156,11 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: pinnedTabsCount) } case let matchingAttribute as CustomHomePageMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - return BooleanMatchingAttribute(value).matches(value: hasCustomHomePage) + return matchingAttribute.evaluate(for: hasCustomHomePage) + case let matchingAttribute as DuckPlayerOnboardedMatchingAttribute: + return matchingAttribute.evaluate(for: isDuckPlayerOnboarded) + case let matchingAttribute as DuckPlayerEnabledMatchingAttribute: + return matchingAttribute.evaluate(for: isDuckPlayerEnabled) default: return commonUserAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) } @@ -201,10 +208,10 @@ public struct CommonUserAttributeMatcher: AttributeMatching { isPrivacyProSubscriptionExpiring: Bool, isPrivacyProSubscriptionExpired: Bool, dismissedMessageIds: [String] - ) { + ) { self.statisticsStore = statisticsStore self.variantManager = variantManager - self.emailManager = emailManager + self.emailManager = emailManager self.appTheme = appTheme self.bookmarksCount = bookmarksCount self.favoritesCount = favoritesCount @@ -224,72 +231,31 @@ public struct CommonUserAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as AppThemeMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return StringMatchingAttribute(value).matches(value: appTheme) + return matchingAttribute.evaluate(for: appTheme) case let matchingAttribute as BookmarksMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: bookmarksCount) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: bookmarksCount) - } + return matchingAttribute.evaluate(for: bookmarksCount) case let matchingAttribute as DaysSinceInstalledMatchingAttribute: guard let installDate = statisticsStore.installDate, let daysSinceInstall = Calendar.current.numberOfDaysBetween(installDate, and: Date()) else { return .fail } - - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: daysSinceInstall) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: daysSinceInstall) - } + return matchingAttribute.evaluate(for: daysSinceInstall) case let matchingAttribute as EmailEnabledMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: emailManager.isSignedIn) + return matchingAttribute.evaluate(for: emailManager.isSignedIn) case let matchingAttribute as FavoritesMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: favoritesCount) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: favoritesCount) - } + return matchingAttribute.evaluate(for: favoritesCount) case let matchingAttribute as DaysSinceNetPEnabledMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: daysSinceNetPEnabled) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: daysSinceNetPEnabled) - } + return matchingAttribute.evaluate(for: daysSinceNetPEnabled) case let matchingAttribute as IsPrivacyProEligibleUserMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: isPrivacyProEligibleUser) + return matchingAttribute.evaluate(for: isPrivacyProEligibleUser) case let matchingAttribute as IsPrivacyProSubscriberUserMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: isPrivacyProSubscriber) + return matchingAttribute.evaluate(for: isPrivacyProSubscriber) case let matchingAttribute as PrivacyProDaysSinceSubscribedMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: privacyProDaysSinceSubscribed) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: privacyProDaysSinceSubscribed) - } + return matchingAttribute.evaluate(for: privacyProDaysSinceSubscribed) case let matchingAttribute as PrivacyProDaysUntilExpiryMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: privacyProDaysUntilExpiry) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: privacyProDaysUntilExpiry) - } + return matchingAttribute.evaluate(for: privacyProDaysUntilExpiry) case let matchingAttribute as PrivacyProPurchasePlatformMatchingAttribute: - return StringArrayMatchingAttribute(matchingAttribute.value).matches(value: privacyProPurchasePlatform ?? "") + return matchingAttribute.evaluate(for: privacyProPurchasePlatform ?? "") case let matchingAttribute as PrivacyProSubscriptionStatusMatchingAttribute: let mappedStatuses = (matchingAttribute.value ?? []).compactMap { status in return PrivacyProSubscriptionStatus(rawValue: status) diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 491caa534..a423eb513 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -176,6 +176,16 @@ struct CustomHomePageMatchingAttribute: SingleValueMatching { var fallback: Bool? } +struct DuckPlayerOnboardedMatchingAttribute: SingleValueMatching { + var value: Bool? + var fallback: Bool? +} + +struct DuckPlayerEnabledMatchingAttribute: SingleValueMatching { + var value: Bool? + var fallback: Bool? +} + struct UnknownMatchingAttribute: MatchingAttribute, Equatable { var fallback: Bool? diff --git a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift index a780931b6..ad19a948e 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/NumericRangeMatching.swift @@ -51,4 +51,11 @@ public extension NumericRangeMatching { fallback: fallback ) } + + func evaluate(for value: Int) -> EvaluationResult { + guard self.value == MatchingAttributeDefaults.intDefaultValue else { + return IntMatchingAttribute(self.value).matches(value: value) + } + return RangeIntMatchingAttribute(min: min, max: max).matches(value: value) + } } diff --git a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift index 902d8e5ed..7ff7dc614 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/SingleValueMatching.swift @@ -40,3 +40,37 @@ public extension SingleValueMatching { self.init(value: value, fallback: fallback) } } + +public extension SingleValueMatching where Value == Bool { + func evaluate(for value: Bool) -> EvaluationResult { + guard let expectedValue = self.value else { + return .fail + } + return BooleanMatchingAttribute(expectedValue).matches(value: value) + } +} + +public extension SingleValueMatching where Value == String { + func evaluate(for value: String) -> EvaluationResult { + guard let expectedValue = self.value else { + return .fail + } + return StringMatchingAttribute(expectedValue).matches(value: value) + } + + func evaluate(for value: String?) -> EvaluationResult { + guard let value, let expectedValue = self.value else { + return .fail + } + return StringMatchingAttribute(expectedValue).matches(value: value) + } +} + +public extension SingleValueMatching where Value == [String] { + func evaluate(for value: String) -> EvaluationResult { + guard let expectedValue = self.value else { + return .fail + } + return StringArrayMatchingAttribute(expectedValue).matches(value: value) + } +} diff --git a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift index c2091402b..c18423d62 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributesPrototypes/StringRangeMatching.swift @@ -53,4 +53,11 @@ extension StringRangeMatching { fallback: fallback ) } + + func evaluate(for value: String) -> EvaluationResult { + guard self.value == MatchingAttributeDefaults.stringDefaultValue else { + return StringMatchingAttribute(self.value).matches(value: value) + } + return RangeStringNumericMatchingAttribute(min: min, max: max).matches(value: value) + } } diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift index aa2887834..63db862ed 100644 --- a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift @@ -109,6 +109,30 @@ class DesktopUserAttributeMatcherTests: XCTestCase { .fail) } + // MARK: - DuckPlayerOnboarded + + func testWhenDuckPlayerOnboardedMatchesThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: DuckPlayerOnboardedMatchingAttribute(value: true, fallback: nil)), + .match) + } + + func testWhenDuckPlayerOnboardedDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: DuckPlayerOnboardedMatchingAttribute(value: false, fallback: nil)), + .fail) + } + + // MARK: - DuckPlayerEnabled + + func testWhenDuckPlayerEnabledMatchesThenReturnMatch() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: DuckPlayerEnabledMatchingAttribute(value: false, fallback: nil)), + .match) + } + + func testWhenDuckPlayerEnabledDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(matcher.evaluate(matchingAttribute: DuckPlayerEnabledMatchingAttribute(value: true, fallback: nil)), + .fail) + } + // MARK: - private func setUpUserAttributeMatcher(dismissedMessageIds: [String] = []) { @@ -130,7 +154,9 @@ class DesktopUserAttributeMatcherTests: XCTestCase { isPrivacyProSubscriptionExpired: false, dismissedMessageIds: dismissedMessageIds, pinnedTabsCount: 3, - hasCustomHomePage: true + hasCustomHomePage: true, + isDuckPlayerOnboarded: true, + isDuckPlayerEnabled: false ) } } From ebaad95127f3c977d5e78964170729de319fe28a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 12 Jul 2024 10:19:19 +0200 Subject: [PATCH 09/12] Fix Swiftlint violation --- Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 660344c02..7ffe641c3 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -227,7 +227,6 @@ public struct CommonUserAttributeMatcher: AttributeMatching { self.dismissedMessageIds = dismissedMessageIds } - // swiftlint:disable:next cyclomatic_complexity public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as AppThemeMatchingAttribute: From 2850a8a82dd3b2dd9e0768dbf3ba91d11db178a0 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 12 Jul 2024 10:22:35 +0200 Subject: [PATCH 10/12] Update remaining matchers for new API --- .../Matchers/UserAttributeMatcher.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 7ffe641c3..1c452dbdd 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -81,11 +81,7 @@ public struct MobileUserAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as WidgetAddedMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: isWidgetInstalled) + return matchingAttribute.evaluate(for: isWidgetInstalled) default: return commonUserAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) } @@ -150,11 +146,7 @@ public struct DesktopUserAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as PinnedTabsMatchingAttribute: - if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { - return IntMatchingAttribute(matchingAttribute.value).matches(value: pinnedTabsCount) - } else { - return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: pinnedTabsCount) - } + return matchingAttribute.evaluate(for: pinnedTabsCount) case let matchingAttribute as CustomHomePageMatchingAttribute: return matchingAttribute.evaluate(for: hasCustomHomePage) case let matchingAttribute as DuckPlayerOnboardedMatchingAttribute: From 08f23d29c36515e11902f1d4da8c5d2151c55e53 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 12 Jul 2024 12:17:17 +0200 Subject: [PATCH 11/12] Address code review feedback --- Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift | 2 +- .../Matchers/CommonUserAttributeMatcherTests.swift | 2 +- .../Matchers/DesktopUserAttributeMatcherTests.swift | 2 +- .../Matchers/MobileUserAttributeMatcherTests.swift | 2 +- .../RemoteMessagingConfigMatcherTests.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift index 50acd49f8..dae59f12b 100644 --- a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift @@ -47,7 +47,7 @@ public struct DesktopAppAttributeMatcher: AttributeMatching { variantManager: VariantManager, isInstalledMacAppStore: Bool ) { - self.isInstalledMacAppStore = isInternalUser + self.isInstalledMacAppStore = isInstalledMacAppStore commonAppAttributeMatcher = .init( bundleId: bundleId, diff --git a/Tests/RemoteMessagingTests/Matchers/CommonUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/CommonUserAttributeMatcherTests.swift index f965d56ce..6de859c21 100644 --- a/Tests/RemoteMessagingTests/Matchers/CommonUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/CommonUserAttributeMatcherTests.swift @@ -46,7 +46,7 @@ class CommonUserAttributeMatcherTests: XCTestCase { currentVariant: MockVariant(name: "zo", weight: 44, isIncluded: { return true }, features: [.dummy])) let emailManagerStorage = MockEmailManagerStorage() - // EmailEnabledMatchingAttribute isSignedIn = true + // Set non-empty username and token so that emailManager's isSignedIn returns true emailManagerStorage.mockUsername = "username" emailManagerStorage.mockToken = "token" diff --git a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift index 63db862ed..0a329bc8a 100644 --- a/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/DesktopUserAttributeMatcherTests.swift @@ -46,7 +46,7 @@ class DesktopUserAttributeMatcherTests: XCTestCase { currentVariant: MockVariant(name: "zo", weight: 44, isIncluded: { return true }, features: [.dummy])) let emailManagerStorage = MockEmailManagerStorage() - // EmailEnabledMatchingAttribute isSignedIn = true + // Set non-empty username and token so that emailManager's isSignedIn returns true emailManagerStorage.mockUsername = "username" emailManagerStorage.mockToken = "token" diff --git a/Tests/RemoteMessagingTests/Matchers/MobileUserAttributeMatcherTests.swift b/Tests/RemoteMessagingTests/Matchers/MobileUserAttributeMatcherTests.swift index 23f9ce379..3eedda82a 100644 --- a/Tests/RemoteMessagingTests/Matchers/MobileUserAttributeMatcherTests.swift +++ b/Tests/RemoteMessagingTests/Matchers/MobileUserAttributeMatcherTests.swift @@ -46,7 +46,7 @@ class MobileUserAttributeMatcherTests: XCTestCase { currentVariant: MockVariant(name: "zo", weight: 44, isIncluded: { return true }, features: [.dummy])) let emailManagerStorage = MockEmailManagerStorage() - // EmailEnabledMatchingAttribute isSignedIn = true + // Set non-empty username and token so that emailManager's isSignedIn returns true emailManagerStorage.mockUsername = "username" emailManagerStorage.mockToken = "token" diff --git a/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift b/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift index a892ab422..8aeead797 100644 --- a/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/RemoteMessagingTests/RemoteMessagingConfigMatcherTests.swift @@ -30,7 +30,7 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { override func setUpWithError() throws { let emailManagerStorage = MockEmailManagerStorage() - // EmailEnabledMatchingAttribute isSignedIn = true + // Set non-empty username and token so that emailManager's isSignedIn returns true emailManagerStorage.mockUsername = "username" emailManagerStorage.mockToken = "token" From 4024e9765f0518033bd4d7c234beea75659f0df6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 12 Jul 2024 12:35:10 +0200 Subject: [PATCH 12/12] Update isInstalledMacAppStore evaluation handler --- Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift index dae59f12b..1805ebbd0 100644 --- a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift @@ -61,11 +61,7 @@ public struct DesktopAppAttributeMatcher: AttributeMatching { public func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as IsInstalledMacAppStoreMatchingAttribute: - guard let value = matchingAttribute.value else { - return .fail - } - - return BooleanMatchingAttribute(value).matches(value: isInstalledMacAppStore) + return matchingAttribute.evaluate(for: isInstalledMacAppStore) default: return commonAppAttributeMatcher.evaluate(matchingAttribute: matchingAttribute) }