Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add desktop specific RMF attributes #883

Merged
merged 13 commits into from
Jul 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ private enum AttributesKey: String, CaseIterable {
case pproPurchasePlatform
case pproSubscriptionStatus
case interactedWithMessage
case installedMacAppStore
case pinnedTabs
case customHomePage
case duckPlayerOnboarded
case duckPlayerEnabled

func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute {
switch self {
Expand All @@ -69,6 +74,11 @@ private enum AttributesKey: String, CaseIterable {
case .pproPurchasePlatform: return PrivacyProPurchasePlatformMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute)
case .pproSubscriptionStatus: return PrivacyProSubscriptionStatusMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute)
case .interactedWithMessage: return InteractedWithMessageMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute)
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)
}
}
}
Expand Down
90 changes: 57 additions & 33 deletions Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,55 @@ 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
Comment on lines +23 to +27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

installedMacAppStore is an "app" attribute, so we're adding platform-specific structs for app attribute matching.


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 = isInstalledMacAppStore

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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle installedMacAppStore here, and handle other attributes via common matcher.

return matchingAttribute.evaluate(for: isInstalledMacAppStore)
default:
return commonAppAttributeMatcher.evaluate(matchingAttribute: matchingAttribute)
}
}
}

public struct CommonAppAttributeMatcher: AttributeMatching {

private let bundleId: String
private let appVersion: String
Expand Down Expand Up @@ -50,46 +98,22 @@ public struct AppAttributeMatcher: 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
}
Expand Down
7 changes: 2 additions & 5 deletions Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
148 changes: 85 additions & 63 deletions Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -83,18 +81,84 @@ 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)
}
}

}

public struct DesktopUserAttributeMatcher: AttributeMatching {
private let pinnedTabsCount: Int
private let hasCustomHomePage: Bool
private let isDuckPlayerOnboarded: Bool
private let isDuckPlayerEnabled: 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],
pinnedTabsCount: Int,
hasCustomHomePage: Bool,
isDuckPlayerOnboarded: Bool,
isDuckPlayerEnabled: Bool
) {
self.pinnedTabsCount = pinnedTabsCount
self.hasCustomHomePage = hasCustomHomePage
self.isDuckPlayerOnboarded = isDuckPlayerOnboarded
self.isDuckPlayerEnabled = isDuckPlayerEnabled

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 PinnedTabsMatchingAttribute:
Copy link
Contributor Author

@ayoy ayoy Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to app attributes matcher, handle pinned tab, custom home page and duck player here and use common matcher for the rest.

return matchingAttribute.evaluate(for: pinnedTabsCount)
case let matchingAttribute as CustomHomePageMatchingAttribute:
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)
}
}
}

public struct CommonUserAttributeMatcher: AttributeMatching {

private enum PrivacyProSubscriptionStatus: String {
Expand Down Expand Up @@ -136,10 +200,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
Expand All @@ -155,78 +219,36 @@ 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:
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
let mappedStatuses = (matchingAttribute.value ?? []).compactMap { status in
return PrivacyProSubscriptionStatus(rawValue: status)
}

Expand Down
Loading
Loading