Skip to content

Commit de42a47

Browse files
authored
Replace duckai ff by filtering subscription entitlements (#6477)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1149059203486286/task/1210908317304409?focus=true ### Description Replace duckai FF by relying on subscription ff and filtering entitlements ### Steps to test this PR _Feature 1_ - [x] Fresh install this branch - [x] Go to settings - [ ] No trace of Duckai in subscription settings (even FF is enabled, BE is not returning the product/entitlement) - [x] Purchase the subscription - [x] Duck.ai is not listed (even FF is enabled, BE is not returning the product/entitlement) _Feature 2_ - [x] Cancel your subscription and remove account - [x] Apply patch attached in https://app.asana.com/1/137249556945/project/1149059203486286/task/1210908317304409?focus=true - [x] Fresh install this branch - [x] Go to settings - [x] Duckai in subscription settings (FF is enabled, BE is returning the product/entitlement) - [x] Purchase the subscription - [x] Duck.ai is listed (FF is enabled, BE is returning the product/entitlement) - [x] Go to Feature flags inventory - [x] Disable duckaiplus inside privacyPro - [x] restart the app - [x] Duckai is not shown in settings (FF is disabled, BE is returning the product/entitlement but we filter) - [x] Cancel the subscription and remove - [x] We don't mention duck.ai in subscription settings (FF is disabled, BE is returning the product/entitlement but we filter) ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent fdf3325 commit de42a47

File tree

8 files changed

+90
-35
lines changed

8 files changed

+90
-35
lines changed

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/feature/DuckChatFeature.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ interface DuckChatFeature {
3636
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
3737
fun self(): Toggle
3838

39-
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
40-
fun duckAiPlus(): Toggle
41-
4239
/**
4340
* @return `true` when the remote config has the "duckAiButtonInBrowser" Duck.ai button in browser
4441
* sub-feature flag enabled

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/subscription/DuckAiPlusSettingsViewModel.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import androidx.lifecycle.viewModelScope
2424
import com.duckduckgo.anvil.annotations.ContributesViewModel
2525
import com.duckduckgo.common.utils.DispatcherProvider
2626
import com.duckduckgo.di.scopes.ViewScope
27-
import com.duckduckgo.duckchat.impl.feature.DuckChatFeature
2827
import com.duckduckgo.duckchat.impl.subscription.DuckAiPlusSettingsViewModel.ViewState.SettingState
2928
import com.duckduckgo.duckchat.impl.subscription.DuckAiPlusSettingsViewModel.ViewState.SettingState.Disabled
3029
import com.duckduckgo.duckchat.impl.subscription.DuckAiPlusSettingsViewModel.ViewState.SettingState.Hidden
@@ -48,7 +47,6 @@ import kotlinx.coroutines.launch
4847
@ContributesViewModel(ViewScope::class)
4948
class DuckAiPlusSettingsViewModel @Inject constructor(
5049
private val subscriptions: Subscriptions,
51-
private val duckChatFeature: DuckChatFeature,
5250
private val dispatcherProvider: DispatcherProvider,
5351
) : ViewModel(), DefaultLifecycleObserver {
5452

@@ -79,11 +77,6 @@ class DuckAiPlusSettingsViewModel @Inject constructor(
7977
super.onCreate(owner)
8078

8179
viewModelScope.launch(dispatcherProvider.io()) {
82-
if (duckChatFeature.duckAiPlus().isEnabled().not()) {
83-
_viewState.update { it.copy(settingState = Hidden) }
84-
return@launch
85-
}
86-
8780
subscriptions.getEntitlementStatus().map { entitlements ->
8881
entitlements.any { product ->
8982
product == DuckAiPlus

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/subscription/DuckAiPlusSettingsViewModelTest.kt

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ package com.duckduckgo.duckchat.impl.subscription
33
import android.annotation.SuppressLint
44
import app.cash.turbine.test
55
import com.duckduckgo.common.test.CoroutineTestRule
6-
import com.duckduckgo.duckchat.impl.feature.DuckChatFeature
76
import com.duckduckgo.duckchat.impl.subscription.DuckAiPlusSettingsViewModel.Command.OpenDuckAiPlusSettings
87
import com.duckduckgo.duckchat.impl.subscription.DuckAiPlusSettingsViewModel.ViewState.SettingState
9-
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
10-
import com.duckduckgo.feature.toggles.api.Toggle.State
118
import com.duckduckgo.subscriptions.api.Product
129
import com.duckduckgo.subscriptions.api.SubscriptionStatus
1310
import com.duckduckgo.subscriptions.api.Subscriptions
@@ -25,29 +22,14 @@ class DuckAiPlusSettingsViewModelTest {
2522
val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
2623

2724
private val subscriptions: Subscriptions = mock()
28-
private val duckChatFeature = FakeFeatureToggleFactory.create(DuckChatFeature::class.java).also {
29-
it.duckAiPlus().setRawStoredState(State(true))
30-
}
3125

3226
private val viewModel: DuckAiPlusSettingsViewModel by lazy {
3327
DuckAiPlusSettingsViewModel(
3428
subscriptions = subscriptions,
35-
duckChatFeature = duckChatFeature,
3629
dispatcherProvider = coroutineTestRule.testDispatcherProvider,
3730
)
3831
}
3932

40-
@Test
41-
fun `when feature flag is disabled then SettingState is Hidden`() = runTest {
42-
duckChatFeature.duckAiPlus().setRawStoredState(State(false))
43-
whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.DuckAiPlus)))
44-
whenever(subscriptions.getSubscriptionStatus()).thenReturn(SubscriptionStatus.AUTO_RENEWABLE)
45-
46-
viewModel.viewState.test {
47-
assertEquals(SettingState.Hidden, expectMostRecentItem().settingState)
48-
}
49-
}
50-
5133
@Test
5234
fun `when onDuckAiPlusClicked then OpenDuckAiSettings command is sent`() = runTest {
5335
viewModel.commands().test {

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
3636
import com.duckduckgo.feature.toggles.api.Toggle.State
3737
import com.duckduckgo.navigation.api.GlobalActivityStarter
3838
import com.duckduckgo.subscriptions.api.Product
39+
import com.duckduckgo.subscriptions.api.Product.DuckAiPlus
3940
import com.duckduckgo.subscriptions.api.SubscriptionStatus
4041
import com.duckduckgo.subscriptions.api.Subscriptions
4142
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BUY_URL
@@ -49,18 +50,23 @@ import com.squareup.anvil.annotations.ContributesBinding
4950
import com.squareup.moshi.JsonAdapter
5051
import com.squareup.moshi.Moshi
5152
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
53+
import dagger.Lazy
5254
import dagger.SingleInstanceIn
5355
import javax.inject.Inject
5456
import kotlinx.coroutines.CoroutineScope
5557
import kotlinx.coroutines.flow.Flow
58+
import kotlinx.coroutines.flow.map
5659
import kotlinx.coroutines.launch
5760
import kotlinx.coroutines.runBlocking
61+
import kotlinx.coroutines.withContext
5862

5963
@ContributesBinding(AppScope::class)
6064
class RealSubscriptions @Inject constructor(
6165
private val subscriptionsManager: SubscriptionsManager,
6266
private val globalActivityStarter: GlobalActivityStarter,
6367
private val pixel: SubscriptionPixelSender,
68+
private val subscriptionsFeature: Lazy<PrivacyProFeature>,
69+
private val dispatcherProvider: DispatcherProvider,
6470
) : Subscriptions {
6571
override suspend fun isSignedIn(): Boolean =
6672
subscriptionsManager.isSignedIn()
@@ -73,7 +79,15 @@ class RealSubscriptions @Inject constructor(
7379
}
7480

7581
override fun getEntitlementStatus(): Flow<List<Product>> {
76-
return subscriptionsManager.entitlements
82+
return subscriptionsManager.entitlements.map { list ->
83+
withContext(dispatcherProvider.io()) {
84+
if (subscriptionsFeature.get().duckAiPlus().isEnabled().not()) {
85+
list.filterNot { entitlement -> entitlement == DuckAiPlus }
86+
} else {
87+
list
88+
}
89+
}
90+
}
7791
}
7892

7993
override suspend fun isEligible(): Boolean {
@@ -94,7 +108,15 @@ class RealSubscriptions @Inject constructor(
94108
override suspend fun getAvailableProducts(): Set<Product> {
95109
return subscriptionsManager.getFeatures()
96110
.mapNotNull { feature -> Product.entries.firstOrNull { it.value == feature } }
97-
.toSet()
111+
.let {
112+
withContext(dispatcherProvider.io()) {
113+
if (subscriptionsFeature.get().duckAiPlus().isEnabled().not()) {
114+
it.filterNot { feature -> feature == DuckAiPlus }
115+
} else {
116+
it
117+
}
118+
}
119+
}.toSet()
98120
}
99121

100122
override fun launchPrivacyPro(context: Context, uri: Uri?) {

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,10 @@ class ProSettingView @JvmOverloads constructor(
172172
}
173173
else -> {
174174
with(binding) {
175-
if (viewState.duckAiEnabled) {
175+
if (viewState.duckAiPlusAvailable) {
176176
subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribeSecure))
177177
} else {
178-
subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribe))
178+
subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribeRebranding))
179179
}
180180
subscriptionBuy.setSecondaryText(getSubscriptionSecondaryText(viewState))
181181
subscriptionGet.setText(getActionButtonText(viewState))

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ class ProSettingViewModel @Inject constructor(
7373
data class ViewState(
7474
val status: SubscriptionStatus = UNKNOWN,
7575
val region: SubscriptionRegion? = null,
76-
val duckAiEnabled: Boolean = false,
7776
val rebrandingEnabled: Boolean = false,
7877
val duckAiPlusAvailable: Boolean = false,
7978
val freeTrialEligible: Boolean = false,
@@ -121,7 +120,6 @@ class ProSettingViewModel @Inject constructor(
121120
viewState.value.copy(
122121
status = subscriptionStatus,
123122
region = region,
124-
duckAiEnabled = duckAiEnabled,
125123
rebrandingEnabled = rebrandingEnabled,
126124
duckAiPlusAvailable = duckAiAvailable,
127125
freeTrialEligible = subscriptionsManager.isFreeTrialEligible(),

subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@
4141
<string name="addToDeviceSecondaryTextWithoutEmailRebranding">Add your subscription to your other devices via Google Play or by linking an email address.</string>
4242
<string name="addToDeviceSecondaryTextWithEmailRebranding">Use the email above to add your subscription on your other devices in the DuckDuckGo app. Go to Settings > Subscription Settings > I Have a Subscription.</string>
4343
<string name="subscriptionSettingRebrandingMessage">Privacy Pro is now just called the DuckDuckGo subscription</string>"
44+
<string name="subscriptionSettingSubscribeRebranding">Protect your connection and identity with one subscription.</string>
4445
</resources>

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.duckduckgo.subscriptions.impl
1818

19+
import android.annotation.SuppressLint
1920
import android.content.Intent
2021
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
2122
import androidx.core.net.toUri
@@ -24,8 +25,11 @@ import androidx.test.platform.app.InstrumentationRegistry
2425
import app.cash.turbine.test
2526
import com.duckduckgo.browser.api.ui.BrowserScreens.SettingsScreenNoParams
2627
import com.duckduckgo.common.test.CoroutineTestRule
28+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
29+
import com.duckduckgo.feature.toggles.api.Toggle.State
2730
import com.duckduckgo.navigation.api.GlobalActivityStarter
2831
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
32+
import com.duckduckgo.subscriptions.api.Product.DuckAiPlus
2933
import com.duckduckgo.subscriptions.api.Product.NetP
3034
import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE
3135
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
@@ -52,6 +56,7 @@ import org.mockito.kotlin.verify
5256
import org.mockito.kotlin.whenever
5357

5458
@RunWith(AndroidJUnit4::class)
59+
@SuppressLint("DenyListedApi")
5560
class RealSubscriptionsTest {
5661

5762
@get:Rule
@@ -63,6 +68,7 @@ class RealSubscriptionsTest {
6368
private val globalActivityStarter: GlobalActivityStarter = mock()
6469
private val pixel: SubscriptionPixelSender = mock()
6570
private lateinit var subscriptions: RealSubscriptions
71+
private val subscriptionFeature: PrivacyProFeature = FakeFeatureToggleFactory.create(PrivacyProFeature::class.java)
6672

6773
private val testSubscriptionOfferList = listOf(
6874
SubscriptionOffer(
@@ -77,7 +83,13 @@ class RealSubscriptionsTest {
7783
fun before() = runTest {
7884
whenever(mockSubscriptionsManager.canSupportEncryption()).thenReturn(true)
7985
whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
80-
subscriptions = RealSubscriptions(mockSubscriptionsManager, globalActivityStarter, pixel)
86+
subscriptions = RealSubscriptions(
87+
mockSubscriptionsManager,
88+
globalActivityStarter,
89+
pixel,
90+
{ subscriptionFeature },
91+
coroutineRule.testDispatcherProvider,
92+
)
8193
}
8294

8395
@Test
@@ -104,6 +116,34 @@ class RealSubscriptionsTest {
104116
}
105117
}
106118

119+
@Test
120+
fun whenGetEntitlementStatusHasEntitlementDuckaiButFFDisabledThenReturnRemoveFromList() = runTest {
121+
subscriptionFeature.duckAiPlus().setRawStoredState(State(false))
122+
whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(AUTO_RENEWABLE)
123+
whenever(mockSubscriptionsManager.entitlements).thenReturn(flowOf(listOf(NetP, DuckAiPlus)))
124+
125+
subscriptions.getEntitlementStatus().test {
126+
val entitlements = awaitItem()
127+
assertFalse(entitlements.contains(DuckAiPlus))
128+
assertTrue(entitlements.size == 1)
129+
cancelAndConsumeRemainingEvents()
130+
}
131+
}
132+
133+
@Test
134+
fun whenGetEntitlementStatusHasEntitlementDuckaiAndFFEnabledThenReturnList() = runTest {
135+
subscriptionFeature.duckAiPlus().setRawStoredState(State(true))
136+
whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(AUTO_RENEWABLE)
137+
whenever(mockSubscriptionsManager.entitlements).thenReturn(flowOf(listOf(NetP, DuckAiPlus)))
138+
139+
subscriptions.getEntitlementStatus().test {
140+
val entitlements = awaitItem()
141+
assertTrue(entitlements.contains(DuckAiPlus))
142+
assertTrue(entitlements.size == 2)
143+
cancelAndConsumeRemainingEvents()
144+
}
145+
}
146+
107147
@Test
108148
fun whenGetEntitlementStatusHasNoEntitlementAndEnabledAndActiveThenReturnEmptyList() = runTest {
109149
whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(AUTO_RENEWABLE)
@@ -115,6 +155,28 @@ class RealSubscriptionsTest {
115155
}
116156
}
117157

158+
@Test
159+
fun whenGetAvailableProductsHasDuckaiButFFDisabledThenReturnRemoveFromList() = runTest {
160+
subscriptionFeature.duckAiPlus().setRawStoredState(State(false))
161+
val subsProducts = setOf(NetP, DuckAiPlus).map { it.value }.toSet()
162+
whenever(mockSubscriptionsManager.getFeatures()).thenReturn(subsProducts)
163+
164+
val products = subscriptions.getAvailableProducts()
165+
assertFalse(products.contains(DuckAiPlus))
166+
assertTrue(products.size == 1)
167+
}
168+
169+
@Test
170+
fun whenGetAvailableProductsHasDuckaiAndFFEnabledThenReturnList() = runTest {
171+
subscriptionFeature.duckAiPlus().setRawStoredState(State(true))
172+
val subsProducts = setOf(NetP, DuckAiPlus).map { it.value }.toSet()
173+
whenever(mockSubscriptionsManager.getFeatures()).thenReturn(subsProducts)
174+
175+
val products = subscriptions.getAvailableProducts()
176+
assertTrue(products.contains(DuckAiPlus))
177+
assertTrue(products.size == 2)
178+
}
179+
118180
@Test
119181
fun whenIsEligibleIfOffersReturnedThenReturnTrueRegardlessOfStatus() = runTest {
120182
whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(UNKNOWN)

0 commit comments

Comments
 (0)