mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-896 Allow user to log out (#129)
This commit is contained in:
parent
6af438a264
commit
aafd32fbc3
21 changed files with 581 additions and 41 deletions
|
@ -36,6 +36,11 @@ interface AuthRepository {
|
|||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Log out the current user.
|
||||
*/
|
||||
fun logout()
|
||||
|
||||
/**
|
||||
* Set the value of [captchaTokenResultFlow].
|
||||
*/
|
||||
|
|
|
@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -88,6 +89,10 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
override fun logout() {
|
||||
mutableAuthStateFlow.update { AuthState.Unauthenticated }
|
||||
}
|
||||
|
||||
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
||||
mutableCaptchaTokenFlow.tryEmit(tokenResult)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import com.x8bit.bitwarden.ui.auth.feature.landing.landingDestinations
|
|||
import com.x8bit.bitwarden.ui.auth.feature.login.loginDestinations
|
||||
import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin
|
||||
|
||||
private const val AUTH_ROUTE: String = "auth"
|
||||
const val AUTH_ROUTE: String = "auth"
|
||||
|
||||
/**
|
||||
* Add auth destinations to the nav graph.
|
||||
|
|
|
@ -5,15 +5,18 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navOptions
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.authDestinations
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuth
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestinations
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlocked
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations
|
||||
|
||||
|
@ -39,7 +42,20 @@ fun RootNavScreen(
|
|||
) {
|
||||
splashDestinations()
|
||||
authDestinations(navController)
|
||||
vaultUnlockedDestinations()
|
||||
vaultUnlockedDestinations(navController)
|
||||
}
|
||||
|
||||
val targetRoute = when (state) {
|
||||
RootNavState.Auth -> AUTH_ROUTE
|
||||
RootNavState.Splash -> SPLASH_ROUTE
|
||||
RootNavState.VaultUnlocked -> VAULT_UNLOCKED_ROUTE
|
||||
}
|
||||
val currentRoute = navController.currentDestination?.rootLevelRoute()
|
||||
|
||||
// Don't navigate if we are already at the correct root. This notably happens during process
|
||||
// death. In this case, the NavHost already restores state, so we don't have to navigate.
|
||||
if (currentRoute == targetRoute) {
|
||||
return
|
||||
}
|
||||
|
||||
// When state changes, navigate to different root navigation state
|
||||
|
@ -47,10 +63,10 @@ fun RootNavScreen(
|
|||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(navController.graph.id) {
|
||||
inclusive = false
|
||||
saveState = true
|
||||
saveState = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
restoreState = false
|
||||
}
|
||||
|
||||
when (state) {
|
||||
|
@ -59,3 +75,20 @@ fun RootNavScreen(
|
|||
RootNavState.VaultUnlocked -> navController.navigateToVaultUnlocked(rootNavOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that returns the highest level route for the given [NavDestination].
|
||||
*
|
||||
* As noted above, this can be removed after upgrading to latest compose navigation, since
|
||||
* the nav args can prevent us from having to do this check.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
private fun NavDestination?.rootLevelRoute(): String? {
|
||||
if (this == null) {
|
||||
return null
|
||||
}
|
||||
if (parent?.route == null) {
|
||||
return route
|
||||
}
|
||||
return parent.rootLevelRoute()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
private const val ACCOUNT_SECURITY_ROUTE = "account_security"
|
||||
|
||||
/**
|
||||
* Add settings destinations to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.accountSecurityDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composable(ACCOUNT_SECURITY_ROUTE) {
|
||||
AccountSecurityScreen(onNavigateBack = onNavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the account security screen.
|
||||
*/
|
||||
fun NavController.navigateToAccountSecurity(navOptions: NavOptions? = null) {
|
||||
navigate(ACCOUNT_SECURITY_ROUTE, navOptions)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowTopAppBar
|
||||
|
||||
/**
|
||||
* Displays the account security screen.
|
||||
*/
|
||||
@Composable
|
||||
fun AccountSecurityScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: AccountSecurityViewModel = hiltViewModel(),
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
AccountSecurityEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
BitwardenOverflowTopAppBar(
|
||||
title = stringResource(id = R.string.account),
|
||||
navigationIcon = painterResource(id = R.drawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.BackClick) }
|
||||
},
|
||||
dropdownMenuItemContent = {},
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AccountSecurityRow(
|
||||
text = R.string.log_out.asText(),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.LogoutClick) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountSecurityRow(
|
||||
text: Text,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = text(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* View model for the account security screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AccountSecurityViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
) : BaseViewModel<Unit, AccountSecurityEvent, AccountSecurityAction>(
|
||||
initialState = Unit,
|
||||
) {
|
||||
override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
|
||||
AccountSecurityAction.LogoutClick -> authRepository.logout()
|
||||
AccountSecurityAction.BackClick -> sendEvent(AccountSecurityEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the account security screen.
|
||||
*/
|
||||
sealed class AccountSecurityEvent {
|
||||
/**
|
||||
* Navigate back.
|
||||
*/
|
||||
data object NavigateBack : AccountSecurityEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the account security screen.
|
||||
*/
|
||||
sealed class AccountSecurityAction {
|
||||
/**
|
||||
* User clicked back button.
|
||||
*/
|
||||
data object BackClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User clicked log out.
|
||||
*/
|
||||
data object LogoutClick : AccountSecurityAction()
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
const val SETTINGS_ROUTE: String = "settings"
|
||||
|
||||
/**
|
||||
* Add settings destinations to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.settingsDestinations(
|
||||
onNavigateToAccountSecurity: () -> Unit,
|
||||
) {
|
||||
composable(SETTINGS_ROUTE) {
|
||||
SettingsScreen(onNavigateToAccountSecurity = onNavigateToAccountSecurity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the settings screen screen.
|
||||
*/
|
||||
fun NavController.navigateToSettings(navOptions: NavOptions? = null) {
|
||||
navigate(SETTINGS_ROUTE, navOptions)
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
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.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.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
|
||||
|
||||
/**
|
||||
* Displays the settings screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onNavigateToAccountSecurity: () -> Unit,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
SettingsEvent.NavigateAccountSecurity -> onNavigateToAccountSecurity.invoke()
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
BitwardenMediumTopAppBar(
|
||||
title = stringResource(id = R.string.settings),
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
)
|
||||
SettingsRow(
|
||||
text = R.string.account.asText(),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SettingsAction.AccountSecurityClick) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsRow(
|
||||
text: Text,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = text(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* View model for the settings screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor() : BaseViewModel<Unit, SettingsEvent, SettingsAction>(
|
||||
initialState = Unit,
|
||||
) {
|
||||
override fun handleAction(action: SettingsAction): Unit = when (action) {
|
||||
SettingsAction.AccountSecurityClick -> handleAccountSecurityClick()
|
||||
}
|
||||
|
||||
private fun handleAccountSecurityClick() {
|
||||
sendEvent(SettingsEvent.NavigateAccountSecurity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the settings screen.
|
||||
*/
|
||||
sealed class SettingsEvent {
|
||||
/**
|
||||
* Navigate to the account security screen.
|
||||
*/
|
||||
data object NavigateAccountSecurity : SettingsEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the settings screen.
|
||||
*/
|
||||
sealed class SettingsAction {
|
||||
/**
|
||||
* User clicked account security.
|
||||
*/
|
||||
data object AccountSecurityClick : SettingsAction()
|
||||
}
|
|
@ -2,12 +2,15 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlocked
|
|||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.navigation
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountSecurityDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToAccountSecurity
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
|
||||
|
||||
private const val VAULT_UNLOCKED_ROUTE: String = "VaultUnlocked"
|
||||
const val VAULT_UNLOCKED_ROUTE: String = "VaultUnlocked"
|
||||
|
||||
/**
|
||||
* Navigate to the vault unlocked screen.
|
||||
|
@ -19,11 +22,16 @@ fun NavController.navigateToVaultUnlocked(navOptions: NavOptions? = null) {
|
|||
/**
|
||||
* Add vault unlocked destinations to the root nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.vaultUnlockedDestinations() {
|
||||
fun NavGraphBuilder.vaultUnlockedDestinations(navController: NavHostController) {
|
||||
navigation(
|
||||
startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE,
|
||||
route = VAULT_UNLOCKED_ROUTE,
|
||||
) {
|
||||
vaultUnlockedNavBarDestination()
|
||||
vaultUnlockedNavBarDestination(
|
||||
onNavigateToAccountSecurity = { navController.navigateToAccountSecurity() },
|
||||
)
|
||||
accountSecurityDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,12 @@ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null)
|
|||
/**
|
||||
* Add vault unlocked destination to the root nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.vaultUnlockedNavBarDestination() {
|
||||
fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||
onNavigateToAccountSecurity: () -> Unit,
|
||||
) {
|
||||
composable(VAULT_UNLOCKED_NAV_BAR_ROUTE) {
|
||||
VaultUnlockedNavBarScreen()
|
||||
VaultUnlockedNavBarScreen(
|
||||
onNavigateToAccountSecurity = onNavigateToAccountSecurity,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ import androidx.navigation.navOptions
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.PlaceholderComposable
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettings
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.settingsDestinations
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_ROUTE
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorDestination
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGenerator
|
||||
|
@ -41,6 +44,7 @@ import kotlinx.parcelize.Parcelize
|
|||
*/
|
||||
@Composable
|
||||
fun VaultUnlockedNavBarScreen(
|
||||
onNavigateToAccountSecurity: () -> Unit,
|
||||
viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
) {
|
||||
|
@ -68,6 +72,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
}
|
||||
VaultUnlockedNavBarScaffold(
|
||||
navController = navController,
|
||||
onNavigateToAccountSecurity = onNavigateToAccountSecurity,
|
||||
generatorTabClickedAction = {
|
||||
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
|
||||
},
|
||||
|
@ -89,6 +94,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun VaultUnlockedNavBarScaffold(
|
||||
onNavigateToAccountSecurity: () -> Unit,
|
||||
navController: NavHostController,
|
||||
vaultTabClickedAction: () -> Unit,
|
||||
sendTabClickedAction: () -> Unit,
|
||||
|
@ -162,7 +168,9 @@ private fun VaultUnlockedNavBarScaffold(
|
|||
vaultDestination()
|
||||
sendDestination()
|
||||
generatorDestination()
|
||||
settingsDestination()
|
||||
settingsDestinations(
|
||||
onNavigateToAccountSecurity = onNavigateToAccountSecurity,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -301,32 +309,3 @@ private fun NavController.navigateToSend(navOptions: NavOptions? = null) {
|
|||
navigate(SEND_ROUTE, navOptions)
|
||||
}
|
||||
// #endregion Send
|
||||
|
||||
// #region Settings
|
||||
/**
|
||||
* TODO: move to settings package (BIT-147)
|
||||
*/
|
||||
private const val SETTINGS_ROUTE = "settings"
|
||||
|
||||
/**
|
||||
* Add settings destination to the nav graph.
|
||||
*
|
||||
* TODO: move to settings package (BIT-147)
|
||||
*/
|
||||
private fun NavGraphBuilder.settingsDestination() {
|
||||
composable(SETTINGS_ROUTE) {
|
||||
PlaceholderComposable(text = "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the generator screen. Note this will only work if generator screen was added
|
||||
* via [settingsDestination].
|
||||
*
|
||||
* TODO: move to settings package (BIT-147)
|
||||
*
|
||||
*/
|
||||
private fun NavController.navigateToSettings(navOptions: NavOptions? = null) {
|
||||
navigate(SETTINGS_ROUTE, navOptions)
|
||||
}
|
||||
// #endregion Settings
|
||||
|
|
9
app/src/main/res/drawable/ic_back.xml
Normal file
9
app/src/main/res/drawable/ic_back.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M20,11.103H7.83L13.42,5.513L12,4.103L4,12.103L12,20.103L13.41,18.693L7.83,13.103H20V11.103Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
|
@ -202,6 +202,31 @@ class AuthRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout should change AuthState to be Unauthenticated`() = runTest {
|
||||
// First login:
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
.returns(Result.success(GetTokenResponseJson.Success(accessToken = ACCESS_TOKEN)))
|
||||
every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
|
||||
// Then call logout:
|
||||
repository.authStateFlow.test {
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
|
||||
repository.logout()
|
||||
assertEquals(AuthState.Unauthenticated, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@test.com"
|
||||
private const val PASSWORD = "password"
|
||||
|
|
|
@ -19,10 +19,10 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(fakeNavHostController.graphId) {
|
||||
inclusive = false
|
||||
saveState = true
|
||||
saveState = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
restoreState = false
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Test
|
||||
|
||||
class AccountSecurityScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on Log out click should send LogoutClick`() {
|
||||
val viewModel: AccountSecurityViewModel = mockk {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AccountSecurityAction.LogoutClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Log out").performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.LogoutClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on back click should send BackClick`() {
|
||||
val viewModel: AccountSecurityViewModel = mockk {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AccountSecurityAction.BackClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.BackClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateAccountSecurity should call onNavigateToAccountSecurity`() {
|
||||
var haveCalledNavigateBack = false
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { eventFlow } returns flowOf(AccountSecurityEvent.NavigateBack)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { haveCalledNavigateBack = true },
|
||||
)
|
||||
}
|
||||
assert(haveCalledNavigateBack)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `on BackClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
authRepository = mockk(),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.BackClick)
|
||||
assertEquals(AccountSecurityEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on LogoutClick should call logout`() = runTest {
|
||||
val authRepository: AuthRepository = mockk {
|
||||
every { logout() } returns Unit
|
||||
}
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
authRepository = authRepository,
|
||||
)
|
||||
viewModel.trySendAction(AccountSecurityAction.LogoutClick)
|
||||
verify { authRepository.logout() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Test
|
||||
|
||||
class SettingsScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on account row click should emit AccountSecurityClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.AccountSecurityClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Account").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.AccountSecurityClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateAccountSecurity should call onNavigateToAccountSecurity`() {
|
||||
var haveCalledNavigateToAccountSecurity = false
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns flowOf(SettingsEvent.NavigateAccountSecurity)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = {
|
||||
haveCalledNavigateToAccountSecurity = true
|
||||
},
|
||||
)
|
||||
}
|
||||
assert(haveCalledNavigateToAccountSecurity)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
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 SettingsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `on AccountSecurityClick should emit NavigateAccountSecurity`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.AccountSecurityClick)
|
||||
assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
onNodeWithText("My vault").performClick()
|
||||
|
@ -52,6 +53,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
|
||||
|
@ -73,6 +75,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
onNodeWithText("Send").performClick()
|
||||
|
@ -93,6 +96,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
|
||||
|
@ -114,6 +118,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
onNodeWithText("Generator").performClick()
|
||||
|
@ -134,6 +139,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
|
||||
|
@ -155,6 +161,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
onNodeWithText("Settings").performClick()
|
||||
|
@ -175,6 +182,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
onNavigateToAccountSecurity = {},
|
||||
)
|
||||
}
|
||||
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
|
||||
|
|
Loading…
Reference in a new issue