diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt index 5565b9bbb..0d44a9a41 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt @@ -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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt index ef4de19fc..ea07d821f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt @@ -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() + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt index 434ab18d9..c8da11984 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt @@ -117,9 +117,11 @@ object PlatformRepositoryModule { featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource, serverConfigRepository: ServerConfigRepository, authDiskSource: AuthDiskSource, + settingsDiskSource: SettingsDiskSource, ): DebugMenuRepository = DebugMenuRepositoryImpl( featureFlagOverrideDiskSource = featureFlagOverrideDiskSource, serverConfigRepository = serverConfigRepository, authDiskSource = authDiskSource, + settingsDiskSource = settingsDiskSource, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt index 6e690cf3f..f65a3fe4a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -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, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt index d833cb0d2..e79702f47 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt @@ -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( 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. diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml index 585b1dc24..0d84a34a2 100644 --- a/app/src/main/res/values/strings_non_localized.xml +++ b/app/src/main/res/values/strings_non_localized.xml @@ -17,5 +17,7 @@ Onboarding Status Override Restart Onboarding 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. + Show Onboarding Carousel + 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. diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt index 1117c8ae8..12bc28097 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt @@ -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(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" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt index 73d481b6f..974b91872 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt @@ -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) } } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index afc8a0ab0..9a59db9e9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -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(any()) } returns flowOf(true) } - private val mockDebugMenuRepository = mockk { + private val mockAuthRepository = mockk(relaxed = true) { + every { hasPendingAccountAddition = true } just runs + } + + private val mockDebugMenuRepository = mockk(relaxed = true) { coEvery { resetFeatureFlagOverrides() } just runs every { updateFeatureFlag(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, ) }