mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
PM-11479 Expired link UI (#3854)
This commit is contained in:
parent
c017c1b10c
commit
b672418c45
10 changed files with 423 additions and 0 deletions
|
@ -17,6 +17,7 @@ import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDest
|
|||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.navigateToEnterpriseSignOn
|
||||
import com.x8bit.bitwarden.ui.auth.feature.environment.environmentDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.environment.navigateToEnvironment
|
||||
import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.expiredRegistrationLinkDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.landing.LANDING_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.landing.landingDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.landing.navigateToLanding
|
||||
|
@ -189,6 +190,23 @@ fun NavGraphBuilder.authGraph(
|
|||
navController.popUpToCompleteRegistration()
|
||||
},
|
||||
)
|
||||
expiredRegistrationLinkDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToStartRegistration = {
|
||||
navController.navigateToStartRegistration(
|
||||
navOptions = navOptions {
|
||||
popUpTo(LANDING_ROUTE)
|
||||
},
|
||||
)
|
||||
},
|
||||
onNavigateToLogin = {
|
||||
navController.navigateToLanding(
|
||||
navOptions = navOptions {
|
||||
popUpTo(LANDING_ROUTE)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
|
||||
private const val EXPIRED_REGISTRATION_LINK_ROUTE = "expired_registration_link"
|
||||
|
||||
/**
|
||||
* Navigate to the expired registration link screen.
|
||||
*/
|
||||
fun NavController.navigateToExpiredRegistrationLinkScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(route = EXPIRED_REGISTRATION_LINK_ROUTE, navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the expired registration link screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.expiredRegistrationLinkDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
onNavigateToLogin: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = EXPIRED_REGISTRATION_LINK_ROUTE,
|
||||
) {
|
||||
ExpiredRegistrationLinkScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToStartRegistration = onNavigateToStartRegistration,
|
||||
onNavigateToLogin = onNavigateToLogin,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
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.navigationBarsPadding
|
||||
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.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.EventsEffect
|
||||
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.appbar.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
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 screen component for the ExpiredRegistrationLink screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ExpiredRegistrationLinkScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLogin: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
viewModel: ExpiredRegistrationLinkViewModel = hiltViewModel(),
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
ExpiredRegistrationLinkEvent.NavigateBack -> onNavigateBack()
|
||||
ExpiredRegistrationLinkEvent.NavigateToLogin -> onNavigateToLogin()
|
||||
ExpiredRegistrationLinkEvent.NavigateToStartRegistration -> {
|
||||
onNavigateToStartRegistration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BitwardenScaffold(
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
ExpiredRegistrationLinkContent(
|
||||
onNavigateToLogin = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.GoToLoginClicked)
|
||||
}
|
||||
},
|
||||
onNavigateToStartRegistration = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ExpiredRegistrationLinkAction.RestartRegistrationClicked,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpiredRegistrationLinkContent(
|
||||
onNavigateToLogin: () -> Unit,
|
||||
onNavigateToStartRegistration: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.expired_link),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.please_restart_registration_or_try_logging_in),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.restart_registration),
|
||||
onClick = onNavigateToStartRegistration,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.log_in_verb),
|
||||
onClick = onNavigateToLogin,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun ExpiredRegistrationLinkScreen_preview() {
|
||||
BitwardenTheme {
|
||||
ExpiredRegistrationLinkContent(
|
||||
onNavigateToLogin = {},
|
||||
onNavigateToStartRegistration = {},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* View model for the [ExpiredRegistrationLinkScreen].
|
||||
*/
|
||||
class ExpiredRegistrationLinkViewModel @Inject constructor() :
|
||||
BaseViewModel<Unit, ExpiredRegistrationLinkEvent, ExpiredRegistrationLinkAction>(
|
||||
initialState = Unit,
|
||||
) {
|
||||
override fun handleAction(action: ExpiredRegistrationLinkAction) {
|
||||
when (action) {
|
||||
ExpiredRegistrationLinkAction.CloseClicked -> handleCloseClicked()
|
||||
ExpiredRegistrationLinkAction.GoToLoginClicked -> handleGoToLoginClicked()
|
||||
ExpiredRegistrationLinkAction.RestartRegistrationClicked -> {
|
||||
handleRestartRegistrationClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRestartRegistrationClicked() {
|
||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateToStartRegistration)
|
||||
}
|
||||
|
||||
private fun handleGoToLoginClicked() {
|
||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateToLogin)
|
||||
}
|
||||
|
||||
private fun handleCloseClicked() {
|
||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model the events that can be sent from the [ExpiredRegistrationLinkViewModel].
|
||||
*/
|
||||
sealed class ExpiredRegistrationLinkEvent {
|
||||
|
||||
/**
|
||||
* Models event to navigate back to the previous screen.
|
||||
*/
|
||||
data object NavigateBack : ExpiredRegistrationLinkEvent()
|
||||
|
||||
/**
|
||||
* Models event to navigate to the login screen.
|
||||
*/
|
||||
data object NavigateToLogin : ExpiredRegistrationLinkEvent()
|
||||
|
||||
/**
|
||||
* Models event to navigate to the start registration screen.
|
||||
*/
|
||||
data object NavigateToStartRegistration : ExpiredRegistrationLinkEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models the actions that can be handled by the [ExpiredRegistrationLinkViewModel].
|
||||
*/
|
||||
sealed class ExpiredRegistrationLinkAction {
|
||||
/**
|
||||
* Indicates the close button was clicked.
|
||||
*/
|
||||
data object CloseClicked : ExpiredRegistrationLinkAction()
|
||||
|
||||
/**
|
||||
* Indicated the restart registration button was clicked.
|
||||
*/
|
||||
data object RestartRegistrationClicked : ExpiredRegistrationLinkAction()
|
||||
|
||||
/**
|
||||
* Indicated the login button was clicked.
|
||||
*/
|
||||
data object GoToLoginClicked : ExpiredRegistrationLinkAction()
|
||||
}
|
|
@ -19,6 +19,7 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE
|
|||
import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
||||
import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.navigateToExpiredRegistrationLinkScreen
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.REMOVE_PASSWORD_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.removePasswordDestination
|
||||
|
@ -95,6 +96,7 @@ fun RootNavScreen(
|
|||
RootNavState.Auth,
|
||||
is RootNavState.CompleteOngoingRegistration,
|
||||
RootNavState.AuthWithWelcome,
|
||||
RootNavState.ExpiredRegistrationLink,
|
||||
-> AUTH_GRAPH_ROUTE
|
||||
|
||||
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
||||
|
@ -156,6 +158,11 @@ fun RootNavScreen(
|
|||
)
|
||||
}
|
||||
|
||||
RootNavState.ExpiredRegistrationLink -> {
|
||||
navController.navigateToAuthGraph(rootNavOptions)
|
||||
navController.navigateToExpiredRegistrationLinkScreen()
|
||||
}
|
||||
|
||||
RootNavState.RemovePassword -> navController.navigateToRemovePassword(rootNavOptions)
|
||||
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
||||
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
||||
|
|
|
@ -302,6 +302,12 @@ sealed class RootNavState : Parcelable {
|
|||
*/
|
||||
@Parcelize
|
||||
data object VaultUnlockedForAuthRequest : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show the expired registration link screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data object ExpiredRegistrationLink : RootNavState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
<string name="invalid_pin">Invalid PIN. Try again.</string>
|
||||
<string name="launch">Launch</string>
|
||||
<string name="log_in">Log In</string>
|
||||
<string name="log_in_verb">Log in</string>
|
||||
<string name="log_in_noun">Login</string>
|
||||
<string name="log_out">Log out</string>
|
||||
<string name="logout_confirmation">Are you sure you want to log out?</string>
|
||||
|
@ -988,4 +989,7 @@ Do you want to switch to this account?</string>
|
|||
<string name="choose_your_master_password">Choose your master password</string>
|
||||
<string name="choose_a_unique_and_strong_password_to_keep_your_information_safe">Choose a unique and strong password to keep your information safe.</string>
|
||||
<string name="minimum_characters">%1$s characters</string>
|
||||
<string name="expired_link">Expired link</string>
|
||||
<string name="please_restart_registration_or_try_logging_in">Please restart registration or try logging in. You may already have an account.</string>
|
||||
<string name="restart_registration">Restart registration</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ExpiredRegistrationLinkScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToLoginCalled = false
|
||||
private var onNavigateToStartRegistrationCalled = false
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<ExpiredRegistrationLinkEvent>()
|
||||
private val viewModel = mockk<ExpiredRegistrationLinkViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
ExpiredRegistrationLinkScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToLogin = { onNavigateToLoginCalled = true },
|
||||
onNavigateToStartRegistration = { onNavigateToStartRegistrationCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClicked sends NavigateBack action`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Close")
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RestartRegistrationClicked sends RestartRegistrationClicked action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Restart registration")
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(ExpiredRegistrationLinkAction.RestartRegistrationClicked) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GoToLoginClicked sends GoToLoginClicked action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Log in")
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(ExpiredRegistrationLinkAction.GoToLoginClicked) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event invokes onNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(ExpiredRegistrationLinkEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event invokes onNavigateToLogin`() {
|
||||
mutableEventFlow.tryEmit(ExpiredRegistrationLinkEvent.NavigateToLogin)
|
||||
assertTrue(onNavigateToLoginCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToStartRegistration event invokes onNavigateToStartRegistration`() {
|
||||
mutableEventFlow.tryEmit(ExpiredRegistrationLinkEvent.NavigateToStartRegistration)
|
||||
assertTrue(onNavigateToStartRegistrationCalled)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ExpiredRegistrationLinkViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `CloseClicked sends NavigateBack event`() = runTest {
|
||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RestartRegistrationClicked sends NavigateToStartRegistration event`() = runTest {
|
||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.RestartRegistrationClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToStartRegistration, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GoToLoginClicked sends NavigateToLogin event`() = runTest {
|
||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.GoToLoginClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToLogin, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -98,6 +98,14 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to expired registration link route works as expected:
|
||||
rootNavStateFlow.value = RootNavState.ExpiredRegistrationLink
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "expired_registration_link",
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to vault locked works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultLocked
|
||||
composeTestRule.runOnIdle {
|
||||
|
|
Loading…
Reference in a new issue