PM-12760 Add way to re-show the onboarding carousel via debug menu (#3999)

This commit is contained in:
Dave Severns 2024-10-03 09:58:37 -04:00 committed by GitHub
parent e2e5042be5
commit 569ffc3583
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 170 additions and 13 deletions

View file

@ -37,4 +37,11 @@ interface DebugMenuRepository {
* Resets the onboarding status to NOT_STARTED for the current active user, if applicable.
*/
fun resetOnboardingStatusForCurrentUser()
/**
* Manipulates the state to force showing the onboarding carousel.
*
* @param userStateUpdateTrigger A passable lambda to trigger a user state update.
*/
fun modifyStateToShowOnboardingCarousel(userStateUpdateTrigger: () -> Unit)
}

View file

@ -4,6 +4,7 @@ import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.getFlagValueOrDefault
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onSubscription
class DebugMenuRepositoryImpl(
private val featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
private val serverConfigRepository: ServerConfigRepository,
private val settingsDiskSource: SettingsDiskSource,
private val authDiskSource: AuthDiskSource,
) : DebugMenuRepository {
@ -53,4 +55,11 @@ class DebugMenuRepositoryImpl(
onboardingStatus = OnboardingStatus.NOT_STARTED,
)
}
override fun modifyStateToShowOnboardingCarousel(
userStateUpdateTrigger: () -> Unit,
) {
settingsDiskSource.hasUserLoggedInOrCreatedAccount = false
userStateUpdateTrigger.invoke()
}
}

View file

@ -117,9 +117,11 @@ object PlatformRepositoryModule {
featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
serverConfigRepository: ServerConfigRepository,
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
): DebugMenuRepository = DebugMenuRepositoryImpl(
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
serverConfigRepository = serverConfigRepository,
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
)
}

View file

@ -42,6 +42,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* Top level screen for the debug menu.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun DebugMenuScreen(
onNavigateBack: () -> Unit,
@ -95,11 +96,21 @@ fun DebugMenuScreen(
},
)
Spacer(Modifier.height(12.dp))
// Pulled these into variable to avoid over-nested formatting in the composable call.
val isRestartOnboardingEnabled = state.featureFlags[FlagKey.OnboardingFlow] as? Boolean
val isRestartOnboardingCarouselEnabled = state
.featureFlags[FlagKey.OnboardingCarousel] as? Boolean
OnboardingOverrideContent(
enabled = (state.featureFlags[FlagKey.OnboardingFlow] as? Boolean) == true,
isRestartOnboardingEnabled = isRestartOnboardingEnabled == true,
onStartOnboarding = remember(viewModel) {
{
viewModel.trySendAction(DebugMenuAction.ReStartOnboarding)
viewModel.trySendAction(DebugMenuAction.RestartOnboarding)
}
},
isCarouselOverrideEnabled = isRestartOnboardingCarouselEnabled == true,
onStartOnboardingCarousel = remember(viewModel) {
{
viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel)
}
},
)
@ -144,10 +155,15 @@ private fun FeatureFlagContent(
}
}
/**
* The content for the onboarding override feature flag.
*/
@Composable
private fun OnboardingOverrideContent(
enabled: Boolean,
isRestartOnboardingEnabled: Boolean,
onStartOnboarding: () -> Unit,
isCarouselOverrideEnabled: Boolean,
onStartOnboardingCarousel: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier) {
@ -161,7 +177,7 @@ private fun OnboardingOverrideContent(
BitwardenFilledButton(
label = stringResource(R.string.restart_onboarding_cta),
onClick = onStartOnboarding,
isEnabled = enabled,
isEnabled = isRestartOnboardingEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
@ -176,6 +192,25 @@ private fun OnboardingOverrideContent(
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(16.dp))
BitwardenFilledButton(
label = stringResource(R.string.restart_onboarding_carousel),
onClick = onStartOnboardingCarousel,
isEnabled = isCarouselOverrideEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.restart_onboarding_carousel_details),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.standardHorizontalMargin(),
style = BitwardenTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
@ -199,6 +234,11 @@ private fun FeatureFlagContent_preview() {
@Composable
private fun OnboardingOverrideContent_preview() {
BitwardenTheme {
OnboardingOverrideContent(onStartOnboarding = {}, enabled = true)
OnboardingOverrideContent(
onStartOnboarding = {},
isRestartOnboardingEnabled = true,
onStartOnboardingCarousel = {},
isCarouselOverrideEnabled = true,
)
}
}

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.debugmenu
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
@ -22,6 +23,7 @@ import javax.inject.Inject
class DebugMenuViewModel @Inject constructor(
featureFlagManager: FeatureFlagManager,
private val debugMenuRepository: DebugMenuRepository,
private val authRepository: AuthRepository,
) : BaseViewModel<DebugMenuState, DebugMenuEvent, DebugMenuAction>(
initialState = DebugMenuState(featureFlags = emptyMap()),
) {
@ -44,10 +46,19 @@ class DebugMenuViewModel @Inject constructor(
is DebugMenuAction.Internal.UpdateFeatureFlagMap -> handleUpdateFeatureFlagMap(action)
DebugMenuAction.NavigateBack -> handleNavigateBack()
DebugMenuAction.ResetFeatureFlagValues -> handleResetFeatureFlagValues()
DebugMenuAction.ReStartOnboarding -> handleResetOnboardingStatus()
DebugMenuAction.RestartOnboarding -> handleResetOnboardingStatus()
DebugMenuAction.RestartOnboardingCarousel -> handleResetOnboardingCarousel()
}
}
private fun handleResetOnboardingCarousel() {
debugMenuRepository.modifyStateToShowOnboardingCarousel(
userStateUpdateTrigger = {
authRepository.hasPendingAccountAddition = true
},
)
}
private fun handleResetOnboardingStatus() {
debugMenuRepository.resetOnboardingStatusForCurrentUser()
}
@ -115,7 +126,12 @@ sealed class DebugMenuAction {
/**
* The user has clicked the restart onboarding button for the onboarding section.
*/
data object ReStartOnboarding : DebugMenuAction()
data object RestartOnboarding : DebugMenuAction()
/**
* The user has clicked the restart onboarding button for the onboarding section.
*/
data object RestartOnboardingCarousel : DebugMenuAction()
/**
* Internal actions not triggered from the UI.

View file

@ -17,5 +17,7 @@
<string name="onboarding_override">Onboarding Status Override</string>
<string name="restart_onboarding_cta">Restart Onboarding</string>
<string name="restart_onboarding_details">This will reset the onboarding status for the current user, if available. After clicking the button you will immediately be redirected to the onboarding flow. Onboarding flag must be enabled.</string>
<string name="restart_onboarding_carousel">Show Onboarding Carousel</string>
<string name="restart_onboarding_carousel_details">This will force the change to app state which will cause the first time carousel to show. The carousel will continue to show for any \"new\" account until a login is completed. May need to exit debug menu manually.</string>
<!-- /Debug Menu -->
</resources>

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
@ -39,9 +40,15 @@ class DebugMenuRepositoryTest {
every { storeOnboardingStatus(any(), any()) } just runs
}
private val mockSettingsDiskSource = mockk<SettingsDiskSource>(relaxed = true) {
every { hasUserLoggedInOrCreatedAccount } returns true
every { hasUserLoggedInOrCreatedAccount = any() } just runs
}
private val debugMenuRepository = DebugMenuRepositoryImpl(
featureFlagOverrideDiskSource = mockFeatureFlagOverrideDiskSource,
serverConfigRepository = mockServerConfigRepository,
settingsDiskSource = mockSettingsDiskSource,
authDiskSource = mockAuthDiskSource,
)
@ -182,6 +189,20 @@ class DebugMenuRepositoryTest {
)
}
}
@Suppress("MaxLineLength")
@Test
fun `modifyStateToShowOnboardingCarousel should set hasUserLoggedInOrCreatedAccount to false and trigger user state update`() {
var lambdaHasBeenCalled = false
val triggerUserStateUpdate = {
lambdaHasBeenCalled = true
}
debugMenuRepository.modifyStateToShowOnboardingCarousel(triggerUserStateUpdate)
verify {
mockSettingsDiskSource.hasUserLoggedInOrCreatedAccount = false
}
assertTrue(lambdaHasBeenCalled)
}
}
private const val TEST_STRING_VALUE = "test"

View file

@ -110,7 +110,7 @@ class DebugMenuScreenTest : BaseComposeTest() {
}
@Test
fun `restart onboarding should send action when clicked`() {
fun `restart onboarding should send action when enabled and clicked`() {
mutableStateFlow.tryEmit(
DebugMenuState(
featureFlags = mapOf(
@ -124,11 +124,11 @@ class DebugMenuScreenTest : BaseComposeTest() {
.assertIsEnabled()
.performClick()
verify { viewModel.trySendAction(DebugMenuAction.ReStartOnboarding) }
verify { viewModel.trySendAction(DebugMenuAction.RestartOnboarding) }
}
@Test
fun `no restart onboarding should not send action when not enabled`() {
fun `restart onboarding should not send action when not enabled`() {
mutableStateFlow.tryEmit(
DebugMenuState(
featureFlags = mapOf(
@ -143,6 +143,43 @@ class DebugMenuScreenTest : BaseComposeTest() {
.assertIsNotEnabled()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(DebugMenuAction.ReStartOnboarding) }
verify(exactly = 0) { viewModel.trySendAction(DebugMenuAction.RestartOnboarding) }
}
@Test
fun `Show onboarding carousel should send action when enabled and clicked`() {
mutableStateFlow.tryEmit(
DebugMenuState(
featureFlags = mapOf(
FlagKey.OnboardingCarousel to true,
),
),
)
composeTestRule
.onNodeWithText("Show Onboarding Carousel", ignoreCase = true)
.performScrollTo()
.assertIsEnabled()
.performClick()
verify { viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) }
}
@Test
fun `show onboarding carousel should not send action when not enabled`() {
mutableStateFlow.tryEmit(
DebugMenuState(
featureFlags = mapOf(
FlagKey.OnboardingCarousel to false,
),
),
)
composeTestRule
.onNodeWithText("Show Onboarding Carousel", ignoreCase = true)
.performScrollTo()
.assertIsNotEnabled()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) }
}
}

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.debugmenu
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
@ -23,10 +24,20 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
every { getFeatureFlagFlow<Boolean>(any()) } returns flowOf(true)
}
private val mockDebugMenuRepository = mockk<DebugMenuRepository> {
private val mockAuthRepository = mockk<AuthRepository>(relaxed = true) {
every { hasPendingAccountAddition = true } just runs
}
private val mockDebugMenuRepository = mockk<DebugMenuRepository>(relaxed = true) {
coEvery { resetFeatureFlagOverrides() } just runs
every { updateFeatureFlag<Boolean>(any(), any()) } just runs
every { resetOnboardingStatusForCurrentUser() } just runs
every {
modifyStateToShowOnboardingCarousel(userStateUpdateTrigger = any())
} answers {
// invokes the passed in lambda, allowing verification in tests.
firstArg<() -> Unit>().invoke()
}
}
@Test
@ -73,13 +84,25 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
@Test
fun `handleResetOnboardingStatus should reset the onboarding status`() {
val viewModel = createViewModel()
viewModel.trySendAction(DebugMenuAction.ReStartOnboarding)
viewModel.trySendAction(DebugMenuAction.RestartOnboarding)
verify { mockDebugMenuRepository.resetOnboardingStatusForCurrentUser() }
}
@Suppress("MaxLineLength")
@Test
fun `handleResetOnboardingCarousel should reset the onboarding carousel and update user state pending account action`() {
val viewModel = createViewModel()
viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel)
verify {
mockDebugMenuRepository.modifyStateToShowOnboardingCarousel(any())
mockAuthRepository.hasPendingAccountAddition = true
}
}
private fun createViewModel(): DebugMenuViewModel = DebugMenuViewModel(
featureFlagManager = mockFeatureFlagManager,
debugMenuRepository = mockDebugMenuRepository,
authRepository = mockAuthRepository,
)
}