mirror of
https://github.com/bitwarden/android.git
synced 2024-11-27 03:49:36 +03:00
PM-12760 Add way to re-show the onboarding carousel via debug menu (#3999)
This commit is contained in:
parent
e2e5042be5
commit
569ffc3583
9 changed files with 170 additions and 13 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,9 +117,11 @@ object PlatformRepositoryModule {
|
|||
featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): DebugMenuRepository = DebugMenuRepositoryImpl(
|
||||
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue