Skip to content

Commit bf2cf88

Browse files
authored
Add ESTABLISHED AppStage guard (#6791)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211343078555471?focus=true ### Description - Guards the `AppStage` once `ESTABLISHED` has been reached - Adds a kill switch to disable the guard ### Steps to test this PR - [ ] Filter logcat for “UserStageStore" - [ ] Fresh install, skip onboarding and add the widget - [ ] Open the widget and send a query - [ ] Verify "UserStageStore: User is already ESTABLISHED, returning ESTABLISHED" - [ ] In feature flag inventory, disable “establishedAppStageGuard" - [ ] Open the widget and send a query - [ ] Verify "UserStageStore: currentStage=ESTABLISHED, newAppStage=DAX_ONBOARDING"
1 parent 2240690 commit bf2cf88

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package com.duckduckgo.app.onboarding.store
1818

19+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
1920
import com.duckduckgo.common.utils.DispatcherProvider
2021
import javax.inject.Inject
2122
import kotlinx.coroutines.flow.Flow
2223
import kotlinx.coroutines.flow.map
2324
import kotlinx.coroutines.withContext
25+
import logcat.LogPriority.DEBUG
26+
import logcat.logcat
2427

2528
interface UserStageStore {
2629
fun userAppStageFlow(): Flow<AppStage>
@@ -33,6 +36,7 @@ interface UserStageStore {
3336
class AppUserStageStore @Inject constructor(
3437
private val userStageDao: UserStageDao,
3538
private val dispatcher: DispatcherProvider,
39+
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
3640
) : UserStageStore {
3741

3842
override fun userAppStageFlow(): Flow<AppStage> {
@@ -48,13 +52,22 @@ class AppUserStageStore @Inject constructor(
4852

4953
override suspend fun stageCompleted(appStage: AppStage): AppStage {
5054
return withContext(dispatcher.io()) {
55+
val currentStage = getUserAppStage()
56+
if (currentStage == AppStage.ESTABLISHED &&
57+
androidBrowserConfigFeature.establishedAppStageGuard().isEnabled()
58+
) {
59+
logcat(DEBUG) { "UserStageStore: User is already ESTABLISHED, returning ESTABLISHED" }
60+
return@withContext AppStage.ESTABLISHED
61+
}
62+
5163
val newAppStage = when (appStage) {
5264
AppStage.NEW -> AppStage.DAX_ONBOARDING
5365
AppStage.DAX_ONBOARDING -> AppStage.ESTABLISHED
5466
AppStage.ESTABLISHED -> AppStage.ESTABLISHED
5567
}
5668

5769
if (newAppStage != appStage) {
70+
logcat(DEBUG) { "UserStageStore: currentStage=$currentStage, newAppStage=$newAppStage" }
5871
userStageDao.updateUserStage(newAppStage)
5972
}
6073

app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,13 @@ interface AndroidBrowserConfigFeature {
170170
*/
171171
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
172172
fun validateIntentResolution(): Toggle
173+
174+
/**
175+
* Kill switch to prevent changing the onboarding stage once AppStage.ESTABLISHED is reached
176+
* @return `true` when the remote config has the global "establishedAppStageGuard" androidBrowserConfig
177+
* sub-feature flag enabled
178+
* If the remote feature is not present defaults to `true`
179+
*/
180+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
181+
fun establishedAppStageGuard(): Toggle
173182
}

app/src/test/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package com.duckduckgo.app.onboarding.store
1818

1919
import app.cash.turbine.test
20+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
2021
import com.duckduckgo.common.test.CoroutineTestRule
22+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
23+
import com.duckduckgo.feature.toggles.api.Toggle.State
2124
import kotlinx.coroutines.flow.MutableSharedFlow
2225
import kotlinx.coroutines.flow.first
2326
import kotlinx.coroutines.flow.flowOf
@@ -35,7 +38,8 @@ class AppUserStageStoreTest {
3538
var coroutineRule = CoroutineTestRule()
3639

3740
private val userStageDao: UserStageDao = mock()
38-
private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider)
41+
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java)
42+
private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, androidBrowserConfigFeature)
3943

4044
@Test
4145
fun whenGetUserAppStageThenReturnCurrentStage() = runTest {
@@ -102,6 +106,48 @@ class AppUserStageStoreTest {
102106
}
103107
}
104108

109+
@Test
110+
fun `when user is established and guard enabled then return established app stage without updating`() = runTest {
111+
givenCurrentStage(AppStage.ESTABLISHED)
112+
113+
val result = testee.stageCompleted(AppStage.NEW)
114+
115+
assertEquals(AppStage.ESTABLISHED, result)
116+
verify(userStageDao).currentUserAppStage()
117+
}
118+
119+
@Test
120+
fun `when user is established and guard disabled then update app stage`() = runTest {
121+
givenCurrentStage(AppStage.ESTABLISHED)
122+
androidBrowserConfigFeature.establishedAppStageGuard().setRawStoredState(State(false))
123+
124+
val result = testee.stageCompleted(AppStage.NEW)
125+
126+
assertEquals(AppStage.DAX_ONBOARDING, result)
127+
verify(userStageDao).updateUserStage(AppStage.DAX_ONBOARDING)
128+
}
129+
130+
@Test
131+
fun `when user is not established and guard enabled then update app stage`() = runTest {
132+
givenCurrentStage(AppStage.NEW)
133+
134+
val result = testee.stageCompleted(AppStage.NEW)
135+
136+
assertEquals(AppStage.DAX_ONBOARDING, result)
137+
verify(userStageDao).updateUserStage(AppStage.DAX_ONBOARDING)
138+
}
139+
140+
@Test
141+
fun `when user is not established and guard disabled then update app stage`() = runTest {
142+
givenCurrentStage(AppStage.NEW)
143+
androidBrowserConfigFeature.establishedAppStageGuard().setRawStoredState(State(false))
144+
145+
val result = testee.stageCompleted(AppStage.NEW)
146+
147+
assertEquals(AppStage.DAX_ONBOARDING, result)
148+
verify(userStageDao).updateUserStage(AppStage.DAX_ONBOARDING)
149+
}
150+
105151
private suspend fun givenCurrentStage(appStage: AppStage) {
106152
whenever(userStageDao.currentUserAppStage()).thenReturn(UserStage(appStage = appStage))
107153
}

0 commit comments

Comments
 (0)