diff --git a/experiments/experiments-api/src/main/java/com/duckduckgo/experiments/api/VariantConfig.kt b/experiments/experiments-api/src/main/java/com/duckduckgo/experiments/api/VariantConfig.kt index c5e4ec07cbfa..fbf52c7a5bad 100644 --- a/experiments/experiments-api/src/main/java/com/duckduckgo/experiments/api/VariantConfig.kt +++ b/experiments/experiments-api/src/main/java/com/duckduckgo/experiments/api/VariantConfig.kt @@ -25,4 +25,5 @@ data class VariantConfig( data class VariantFilters( val locale: List = emptyList(), val androidVersion: List = emptyList(), + val privacyProEligible: Boolean? = null, ) diff --git a/experiments/experiments-impl/build.gradle b/experiments/experiments-impl/build.gradle index 526c0363b01c..b7e8ac5162fd 100644 --- a/experiments/experiments-impl/build.gradle +++ b/experiments/experiments-impl/build.gradle @@ -29,10 +29,10 @@ dependencies { implementation project(path: ':di') implementation project(path: ':common-utils') - implementation project(path: ':experiments-api') implementation project(path: ':statistics-api') implementation project(path: ':app-build-config-api') - implementation project(path: ':browser-api') + implementation project(path: ':experiments-api') + implementation project(path: ':subscriptions-api') // Room implementation AndroidX.room.runtime diff --git a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/ExperimentFiltersManager.kt b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/ExperimentFiltersManager.kt index 6a560b987051..e54a682b865e 100644 --- a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/ExperimentFiltersManager.kt +++ b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/ExperimentFiltersManager.kt @@ -17,13 +17,17 @@ package com.duckduckgo.experiments.impl import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.experiments.api.VariantConfig import com.duckduckgo.experiments.impl.ExperimentFiltersManagerImpl.ExperimentFilterType.ANDROID_VERSION import com.duckduckgo.experiments.impl.ExperimentFiltersManagerImpl.ExperimentFilterType.LOCALE +import com.duckduckgo.experiments.impl.ExperimentFiltersManagerImpl.ExperimentFilterType.PRIVACY_PRO_ELIGIBLE +import com.duckduckgo.subscriptions.api.Subscriptions import com.squareup.anvil.annotations.ContributesBinding import java.util.Locale import javax.inject.Inject +import kotlinx.coroutines.runBlocking interface ExperimentFiltersManager { fun addFilters(entity: VariantConfig): (AppBuildConfig) -> Boolean @@ -32,6 +36,8 @@ interface ExperimentFiltersManager { @ContributesBinding(AppScope::class) class ExperimentFiltersManagerImpl @Inject constructor( private val appBuildConfig: AppBuildConfig, + private val subscriptions: Subscriptions, + private val dispatcherProvider: DispatcherProvider, ) : ExperimentFiltersManager { override fun addFilters(entity: VariantConfig): (AppBuildConfig) -> Boolean { if (entity.variantKey == "sc" || entity.variantKey == "se") { @@ -41,6 +47,7 @@ class ExperimentFiltersManagerImpl @Inject constructor( val filters: MutableMap = mutableMapOf( LOCALE to true, ANDROID_VERSION to true, + PRIVACY_PRO_ELIGIBLE to true, ) if (!entity.filters?.locale.isNullOrEmpty()) { @@ -51,6 +58,10 @@ class ExperimentFiltersManagerImpl @Inject constructor( val userAndroidVersion = appBuildConfig.sdkInt.toString() filters[ANDROID_VERSION] = entity.filters!!.androidVersion.contains(userAndroidVersion) } + if (entity.filters?.privacyProEligible != null) { + val privacyProEligible = runBlocking(dispatcherProvider.io()) { subscriptions.isEligible() } + filters[PRIVACY_PRO_ELIGIBLE] = entity.filters?.privacyProEligible == privacyProEligible + } return { filters.filter { !it.value }.isEmpty() } } @@ -79,5 +90,6 @@ class ExperimentFiltersManagerImpl @Inject constructor( enum class ExperimentFilterType { LOCALE, ANDROID_VERSION, + PRIVACY_PRO_ELIGIBLE, } } diff --git a/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/ExperimentFiltersManagerImplTest.kt b/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/ExperimentFiltersManagerImplTest.kt index 70df4947830d..ba38aaa59ced 100644 --- a/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/ExperimentFiltersManagerImplTest.kt +++ b/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/ExperimentFiltersManagerImplTest.kt @@ -17,26 +17,36 @@ package com.duckduckgo.experiments.impl import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.experiments.api.VariantConfig import com.duckduckgo.experiments.api.VariantFilters +import com.duckduckgo.subscriptions.api.Subscriptions import java.util.Locale import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever class ExperimentFiltersManagerImplTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + private lateinit var testee: ExperimentFiltersManager private val mockAppBuildConfig: AppBuildConfig = mock() + private val mockSubscriptions: Subscriptions = mock() @Before fun setup() { testee = ExperimentFiltersManagerImpl( mockAppBuildConfig, + mockSubscriptions, + coroutineRule.testDispatcherProvider, ) } @@ -75,11 +85,32 @@ class ExperimentFiltersManagerImplTest { } @Test - fun whenVariantComplyWithBothFiltersThenAddFiltersReturnsTrue() { + fun whenVariantComplyWithPrivacyProEligibleFilterThenAddFiltersReturnsTrue() = runTest { + whenever(mockSubscriptions.isEligible()).thenReturn(true) + val testEntity = addActiveVariant(privacyProEligible = true) + + assertTrue(testee.addFilters(testEntity).invoke(mockAppBuildConfig)) + } + + @Test + fun whenVariantDoesNotComplyWithPrivacyProEligibleFilterThenAddFiltersReturnsFalse() = runTest { + whenever(mockSubscriptions.isEligible()).thenReturn(false) + val testEntity = addActiveVariant(privacyProEligible = true) + + assertFalse(testee.addFilters(testEntity).invoke(mockAppBuildConfig)) + } + + @Test + fun whenVariantComplyWithAllFiltersThenAddFiltersReturnsTrue() = runTest { val locale = Locale("en", "US") Locale.setDefault(locale) whenever(mockAppBuildConfig.sdkInt).thenReturn(33) - val testEntity = addActiveVariant(localeFilter = listOf("en_US"), androidVersionFilter = listOf("33", "34")) + whenever(mockSubscriptions.isEligible()).thenReturn(false) + val testEntity = addActiveVariant( + localeFilter = listOf("en_US"), + androidVersionFilter = listOf("33", "34"), + privacyProEligible = false, + ) assertTrue(testee.addFilters(testEntity).invoke(mockAppBuildConfig)) } @@ -104,10 +135,26 @@ class ExperimentFiltersManagerImplTest { assertFalse(testee.addFilters(testEntity).invoke(mockAppBuildConfig)) } + @Test + fun whenVariantComplyWithLocaleAndAndroidVersionFiltersAndDoesNotComplyWithPrivacyProEligibleThenAddFiltersReturnsFalse() = runTest { + val locale = Locale("en", "US") + Locale.setDefault(locale) + whenever(mockAppBuildConfig.sdkInt).thenReturn(33) + whenever(mockSubscriptions.isEligible()).thenReturn(true) + val testEntity = addActiveVariant( + localeFilter = listOf("en_US"), + androidVersionFilter = listOf("33", "34"), + privacyProEligible = false, + ) + + assertFalse(testee.addFilters(testEntity).invoke(mockAppBuildConfig)) + } + private fun addActiveVariant( localeFilter: List = listOf(), androidVersionFilter: List = listOf(), + privacyProEligible: Boolean? = null, ): VariantConfig { - return VariantConfig("key", 1.0, VariantFilters(localeFilter, androidVersionFilter)) + return VariantConfig("key", 1.0, VariantFilters(localeFilter, androidVersionFilter, privacyProEligible)) } } diff --git a/statistics/statistics-impl/build.gradle b/statistics/statistics-impl/build.gradle index 13ca59fe625c..ca2540e548a6 100644 --- a/statistics/statistics-impl/build.gradle +++ b/statistics/statistics-impl/build.gradle @@ -34,7 +34,6 @@ dependencies { implementation project(path: ':app-build-config-api') implementation project(path: ':browser-api') implementation project(path: ':autofill-api') - implementation project(path: ':experiments-api') implementation project(path: ':privacy-config-api') implementation project(path: ':data-store-api') implementation project(path: ':anrs-api') diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/RxPixelSender.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/RxPixelSender.kt index a34e6720b10a..ba4050fff1b8 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/RxPixelSender.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/RxPixelSender.kt @@ -26,7 +26,6 @@ import com.duckduckgo.app.statistics.store.PixelFiredRepository import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.common.utils.device.DeviceInfo import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.experiments.api.VariantManager import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn @@ -51,7 +50,6 @@ class RxPixelSender @Inject constructor( private val api: PixelService, private val pendingPixelDao: PendingPixelDao, private val statisticsDataStore: StatisticsDataStore, - private val variantManager: VariantManager, private val deviceInfo: DeviceInfo, private val statisticsLibraryConfig: StatisticsLibraryConfig?, private val pixelFiredRepository: PixelFiredRepository, @@ -159,7 +157,7 @@ class RxPixelSender @Inject constructor( return defaultParameters.plus(parameters) } - private fun getAtbInfo() = statisticsDataStore.atb?.formatWithVariant(variantManager.getVariantKey()) ?: "" + private fun getAtbInfo() = statisticsDataStore.atb?.formatWithVariant(statisticsDataStore.variant) ?: "" private fun getDeviceFactor() = deviceInfo.formFactor().description diff --git a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/RxPixelSenderTest.kt b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/RxPixelSenderTest.kt index 80c304da37ac..e7ea93b721a8 100644 --- a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/RxPixelSenderTest.kt +++ b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/RxPixelSenderTest.kt @@ -41,7 +41,6 @@ import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule import com.duckduckgo.common.utils.device.DeviceInfo -import com.duckduckgo.experiments.api.VariantManager import io.reactivex.Completable import java.util.concurrent.TimeoutException import kotlinx.coroutines.test.runTest @@ -74,9 +73,6 @@ class RxPixelSenderTest { @Mock val mockStatisticsDataStore: StatisticsDataStore = mock() - @Mock - val mockVariantManager: VariantManager = mock() - @Mock val mockDeviceInfo: DeviceInfo = mock() @@ -97,7 +93,6 @@ class RxPixelSenderTest { api, pendingPixelDao, mockStatisticsDataStore, - mockVariantManager, mockDeviceInfo, object : StatisticsLibraryConfig { override fun shouldFirePixelsAsDev() = true @@ -397,7 +392,7 @@ class RxPixelSenderTest { } private fun givenVariant(variantKey: String) { - whenever(mockVariantManager.getVariantKey()).thenReturn(variantKey) + whenever(mockStatisticsDataStore.variant).thenReturn(variantKey) } private fun givenAtbVariant(atb: Atb) {