From 39df103f125ac218a760a65dccdde9fb383e18be Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:23:24 -0400 Subject: [PATCH] [PM-10632] Setup complete screen for new onboarding (#3921) --- .../datasource/disk/model/OnboardingStatus.kt | 6 + .../accountsetup/SetupCompleteScreen.kt | 134 ++++++++++++++++++ .../accountsetup/SetupCompleteViewModel.kt | 51 +++++++ .../feature/rootnav/RootNavViewModel.kt | 1 + .../res/drawable-night/img_setup_complete.xml | 44 ++++++ .../main/res/drawable/img_setup_complete.xml | 44 ++++++ app/src/main/res/values/strings.xml | 2 + .../accountsetup/SetupCompleteScreenTest.kt | 39 +++++ .../SetupCompleteViewModelTest.kt | 61 ++++++++ 9 files changed, 382 insertions(+) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModel.kt create mode 100644 app/src/main/res/drawable-night/img_setup_complete.xml create mode 100644 app/src/main/res/drawable/img_setup_complete.xml create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreenTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModelTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt index 4e3e71ed563..a870a6a21fd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt @@ -27,6 +27,12 @@ enum class OnboardingStatus { @SerialName("autofillSetup") AUTOFILL_SETUP, + /** + * The user is completing the final step of the onboarding process. + */ + @SerialName("finalStep") + FINAL_STEP, + /** * The user has completed all onboarding steps. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt new file mode 100644 index 00000000000..12aa9be6034 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt @@ -0,0 +1,134 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton +import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * Top level composable for the setup complete screen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SetupCompleteScreen( + viewModel: SetupCompleteViewModel = hiltViewModel(), +) { + val setupCompleteAction: () -> Unit = remember(viewModel) { + { + viewModel.trySendAction(SetupCompleteAction.CompleteSetup) + } + } + + // Handle system back action to complete the setup. + BackHandler(onBack = setupCompleteAction) + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + BitwardenScaffold( + topBar = { + BitwardenTopAppBar( + scrollBehavior = scrollBehavior, + title = stringResource(R.string.account_setup), + navigationIcon = null, + ) + }, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + ) { innerPadding -> + SetupCompleteContent( + modifier = Modifier + .padding(innerPadding) + .verticalScroll(rememberScrollState()), + onContinue = setupCompleteAction, + ) + } +} + +@Composable +private fun SetupCompleteContent( + modifier: Modifier = Modifier, + onContinue: () -> Unit, +) { + Column( + modifier = modifier, + ) { + Spacer(Modifier.height(32.dp)) + Image( + painter = rememberVectorPainter(R.drawable.img_setup_complete), + contentDescription = null, + modifier = Modifier + .align(CenterHorizontally) + .standardHorizontalMargin(), + ) + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(R.string.youre_all_set), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier + .align(CenterHorizontally) + .standardHorizontalMargin(), + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.what_bitwarden_has_to_offer), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier + .align(CenterHorizontally) + .standardHorizontalMargin(), + ) + Spacer(Modifier.height(24.dp)) + BitwardenFilledButton( + label = stringResource(R.string.continue_text), + onClick = onContinue, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SetupCompleteContent_preview() { + BitwardenTheme { + Surface { + SetupCompleteContent( + modifier = Modifier.fillMaxSize(), + onContinue = {}, + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModel.kt new file mode 100644 index 00000000000..24f4ab1b47c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModel.kt @@ -0,0 +1,51 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * ViewModel for the [SetupCompleteScreen] + */ +@HiltViewModel +class SetupCompleteViewModel @Inject constructor( + private val authRepository: AuthRepository, +) : BaseViewModel( + initialState = run { + val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId + SetupCompleteState(userId = userId) + }, +) { + override fun handleAction(action: SetupCompleteAction) { + when (action) { + is SetupCompleteAction.CompleteSetup -> handleCompleteSetup() + } + } + + private fun handleCompleteSetup() { + authRepository.setOnboardingStatus( + userId = state.userId, + status = OnboardingStatus.COMPLETE, + ) + } +} + +/** + * State for the [SetupCompleteScreen] + */ +data class SetupCompleteState( + val userId: String, +) + +/** + * Model user actions for the [SetupCompleteScreen] + */ +sealed class SetupCompleteAction { + + /** + * The user has performed an action to confirm that they are done setting up their account. + */ + data object CompleteSetup : SetupCompleteAction() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 0a7abd97b8c..8288cad072a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -98,6 +98,7 @@ class RootNavViewModel @Inject constructor( -> RootNavState.OnboardingAccountLockSetup OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup OnboardingStatus.COMPLETE -> throw IllegalStateException("Should not have entered here.") + OnboardingStatus.FINAL_STEP -> TODO("PM-12076 complete navigation wiring") } } diff --git a/app/src/main/res/drawable-night/img_setup_complete.xml b/app/src/main/res/drawable-night/img_setup_complete.xml new file mode 100644 index 00000000000..a4bf9712080 --- /dev/null +++ b/app/src/main/res/drawable-night/img_setup_complete.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_setup_complete.xml b/app/src/main/res/drawable/img_setup_complete.xml new file mode 100644 index 00000000000..4cf89f5dd40 --- /dev/null +++ b/app/src/main/res/drawable/img_setup_complete.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e098b050eef..1903b814687 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1002,4 +1002,6 @@ Do you want to switch to this account? Turn on later Turn on autofill later? You can return to complete this step anytime in Settings. + You can now use autofill to log into apps and websites using your saved passwords. Now, you can explore everything else Bitwarden has to offer. + You\'re all set! diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreenTest.kt new file mode 100644 index 00000000000..ebcba94ced1 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreenTest.kt @@ -0,0 +1,39 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test + +class SetupCompleteScreenTest : BaseComposeTest() { + + private val viewModel = mockk(relaxed = true) + + @Before + fun setup() { + setContentWithBackDispatcher { + SetupCompleteScreen(viewModel = viewModel) + } + } + + @Test + fun `When continue button clicked sends CompleteSetup action`() { + composeTestRule + .onNodeWithText("Continue") + .performScrollTo() + .performClick() + + verify { viewModel.trySendAction(SetupCompleteAction.CompleteSetup) } + } + + @Test + fun `When system back behavior is triggered sends CompleteSetup action`() { + backDispatcher?.onBackPressed() + + verify { viewModel.trySendAction(SetupCompleteAction.CompleteSetup) } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModelTest.kt new file mode 100644 index 00000000000..c07ccc2837c --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteViewModelTest.kt @@ -0,0 +1,61 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class SetupCompleteViewModelTest : BaseViewModelTest() { + + private val mockUserState = mockk { + every { activeUserId } returns DEFAULT_USER_ID + } + private val mutableUserStateFlow = MutableStateFlow(mockUserState) + private val authRepository: AuthRepository = mockk(relaxed = true) { + every { userStateFlow } returns mutableUserStateFlow + } + + @Test + fun `When user state has no active accounts then throw IllegalStateException`() { + mutableUserStateFlow.value = null + assertThrows { + createViewModel() + } + } + + @Test + fun `When user state has active account then ViewModel state should contain active user ID`() { + val viewModel = createViewModel() + assertEquals( + SetupCompleteState(userId = DEFAULT_USER_ID), + viewModel.stateFlow.value, + ) + } + + @Test + fun `When CompleteSetup action is sent user state is updated with onboarding COMPLETE`() { + val viewModel = createViewModel() + assertEquals( + SetupCompleteState(userId = DEFAULT_USER_ID), + viewModel.stateFlow.value, + ) + viewModel.trySendAction(SetupCompleteAction.CompleteSetup) + verify { + authRepository.setOnboardingStatus( + DEFAULT_USER_ID, + OnboardingStatus.COMPLETE, + ) + } + } + + private fun createViewModel() = SetupCompleteViewModel(authRepository = authRepository) +} + +private const val DEFAULT_USER_ID = "userId"