PM-10242 PM-10243 PM-10244 PM-10245 PM-10246: Welcome carousel (#3657)
Some checks failed
Crowdin Push / Crowdin Push (push) Waiting to run
Scan / Check PR run (push) Failing after 0s
Scan / SAST scan (push) Has been skipped
Scan / Quality scan (push) Has been skipped
Test / Check PR run (push) Failing after 0s
Test / Test (push) Has been skipped

This commit is contained in:
Shannon Draeker 2024-08-07 16:12:46 -04:00 committed by GitHub
parent e598fe5714
commit f17289a104
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1026 additions and 8 deletions

View file

@ -130,6 +130,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/ */
val organizations: List<SyncResponseJson.Profile.Organization> val organizations: List<SyncResponseJson.Profile.Organization>
/**
* Whether or not the welcome carousel should be displayed, based on the feature flag and
* whether the user has ever logged in or created an account before.
*/
val showWelcomeCarousel: Boolean
/** /**
* Clears the pending deletion state that occurs when the an account is successfully deleted. * Clears the pending deletion state that occurs when the an account is successfully deleted.
*/ */

View file

@ -77,9 +77,11 @@ import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -140,6 +142,7 @@ class AuthRepositoryImpl(
private val trustedDeviceManager: TrustedDeviceManager, private val trustedDeviceManager: TrustedDeviceManager,
private val userLogoutManager: UserLogoutManager, private val userLogoutManager: UserLogoutManager,
private val policyManager: PolicyManager, private val policyManager: PolicyManager,
private val featureFlagManager: FeatureFlagManager,
pushManager: PushManager, pushManager: PushManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
) : AuthRepository, ) : AuthRepository,
@ -318,6 +321,10 @@ class AuthRepositoryImpl(
override val organizations: List<SyncResponseJson.Profile.Organization> override val organizations: List<SyncResponseJson.Profile.Organization>
get() = activeUserId?.let { authDiskSource.getOrganizations(it) }.orEmpty() get() = activeUserId?.let { authDiskSource.getOrganizations(it) }.orEmpty()
override val showWelcomeCarousel: Boolean
get() = !settingsRepository.hasUserLoggedInOrCreatedAccount &&
featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel)
init { init {
pushManager pushManager
.syncOrgKeysFlow .syncOrgKeysFlow

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -52,6 +53,7 @@ object AuthRepositoryModule {
userLogoutManager: UserLogoutManager, userLogoutManager: UserLogoutManager,
pushManager: PushManager, pushManager: PushManager,
policyManager: PolicyManager, policyManager: PolicyManager,
featureFlagManager: FeatureFlagManager,
): AuthRepository = AuthRepositoryImpl( ): AuthRepository = AuthRepositoryImpl(
accountsService = accountsService, accountsService = accountsService,
devicesService = devicesService, devicesService = devicesService,
@ -70,5 +72,6 @@ object AuthRepositoryModule {
userLogoutManager = userLogoutManager, userLogoutManager = userLogoutManager,
pushManager = pushManager, pushManager = pushManager,
policyManager = policyManager, policyManager = policyManager,
featureFlagManager = featureFlagManager,
) )
} }

View file

@ -14,6 +14,7 @@ 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.environment.navigateToEnvironment
import com.x8bit.bitwarden.ui.auth.feature.landing.LANDING_ROUTE 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.landingDestination
import com.x8bit.bitwarden.ui.auth.feature.landing.navigateToLanding
import com.x8bit.bitwarden.ui.auth.feature.login.loginDestination import com.x8bit.bitwarden.ui.auth.feature.login.loginDestination
import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination
@ -25,6 +26,7 @@ import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
import com.x8bit.bitwarden.ui.auth.feature.setpassword.setPasswordDestination import com.x8bit.bitwarden.ui.auth.feature.setpassword.setPasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
import com.x8bit.bitwarden.ui.auth.feature.welcome.welcomeDestination
const val AUTH_GRAPH_ROUTE: String = "auth_graph" const val AUTH_GRAPH_ROUTE: String = "auth_graph"
@ -32,7 +34,9 @@ const val AUTH_GRAPH_ROUTE: String = "auth_graph"
* Add auth destinations to the nav graph. * Add auth destinations to the nav graph.
*/ */
@Suppress("LongMethod") @Suppress("LongMethod")
fun NavGraphBuilder.authGraph(navController: NavHostController) { fun NavGraphBuilder.authGraph(
navController: NavHostController,
) {
navigation( navigation(
startDestination = LANDING_ROUTE, startDestination = LANDING_ROUTE,
route = AUTH_GRAPH_ROUTE, route = AUTH_GRAPH_ROUTE,
@ -72,6 +76,10 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
navController.navigateToEnvironment() navController.navigateToEnvironment()
}, },
) )
welcomeDestination(
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
onNavigateToLogin = { navController.navigateToLanding() },
)
loginDestination( loginDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToMasterPasswordHint = { emailAddress -> onNavigateToMasterPasswordHint = { emailAddress ->

View file

@ -0,0 +1,32 @@
package com.x8bit.bitwarden.ui.auth.feature.welcome
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions
private const val WELCOME_ROUTE: String = "welcome"
/**
* Navigate to the welcome screen.
*/
fun NavController.navigateToWelcome(navOptions: NavOptions? = null) {
this.navigate(WELCOME_ROUTE, navOptions)
}
/**
* Add the Welcome screen to the nav graph.
*/
fun NavGraphBuilder.welcomeDestination(
onNavigateToCreateAccount: () -> Unit,
onNavigateToLogin: () -> Unit,
) {
composableWithStayTransitions(
route = WELCOME_ROUTE,
) {
WelcomeScreen(
onNavigateToCreateAccount = onNavigateToCreateAccount,
onNavigateToLogin = onNavigateToLogin,
)
}
}

View file

@ -0,0 +1,272 @@
package com.x8bit.bitwarden.ui.auth.feature.welcome
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
/**
* Top level composable for the welcome screen.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun WelcomeScreen(
onNavigateToCreateAccount: () -> Unit,
onNavigateToLogin: () -> Unit,
viewModel: WelcomeViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val pagerState = rememberPagerState(pageCount = { state.pages.size })
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is WelcomeEvent.UpdatePager -> {
pagerState.animateScrollToPage(event.index)
}
WelcomeEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
WelcomeEvent.NavigateToLogin -> onNavigateToLogin()
}
}
BitwardenScaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
WelcomeScreenContent(
state = state,
pagerState = pagerState,
onPagerSwipe = remember(viewModel) {
{ viewModel.trySendAction(WelcomeAction.PagerSwipe(it)) }
},
onDotClick = remember(viewModel) {
{ viewModel.trySendAction(WelcomeAction.DotClick(it)) }
},
onCreateAccountClick = remember(viewModel) {
{ viewModel.trySendAction(WelcomeAction.CreateAccountClick) }
},
onLoginClick = remember(viewModel) {
{ viewModel.trySendAction(WelcomeAction.LoginClick) }
},
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun WelcomeScreenContent(
state: WelcomeState,
pagerState: PagerState,
onPagerSwipe: (Int) -> Unit,
onDotClick: (Int) -> Unit,
onCreateAccountClick: () -> Unit,
onLoginClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
val horizontalPadding = if (isLandscape) 128.dp else 16.dp
LaunchedEffect(pagerState.currentPage) {
onPagerSwipe(pagerState.currentPage)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.weight(1f))
HorizontalPager(state = pagerState) { index ->
if (isLandscape) {
WelcomeCardLandscape(
state = state.pages[index],
modifier = Modifier.padding(horizontal = horizontalPadding),
)
} else {
WelcomeCardPortrait(
state = state.pages[index],
modifier = Modifier.padding(horizontal = horizontalPadding),
)
}
}
Spacer(modifier = Modifier.weight(1f))
IndicatorDots(
selectedIndexProvider = { state.index },
totalCount = state.pages.size,
onDotClick = onDotClick,
modifier = Modifier
.padding(bottom = 32.dp)
.height(44.dp),
)
BitwardenFilledButton(
label = stringResource(id = R.string.create_account),
onClick = onCreateAccountClick,
modifier = Modifier
.padding(horizontal = horizontalPadding)
.fillMaxWidth(),
)
BitwardenTextButton(
label = stringResource(id = R.string.log_in),
onClick = onLoginClick,
modifier = Modifier
.padding(horizontal = horizontalPadding)
.padding(bottom = 32.dp),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@Composable
private fun WelcomeCardLandscape(
state: WelcomeState.WelcomeCard,
modifier: Modifier = Modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Image(
painter = rememberVectorPainter(id = state.imageRes),
contentDescription = null,
modifier = Modifier.size(132.dp),
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(start = 40.dp),
) {
Text(
text = stringResource(id = state.titleRes),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 16.dp),
)
Text(
text = stringResource(id = state.messageRes),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
private fun WelcomeCardPortrait(
state: WelcomeState.WelcomeCard,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier,
) {
Image(
painter = rememberVectorPainter(id = state.imageRes),
contentDescription = null,
modifier = Modifier.size(200.dp),
)
Text(
text = stringResource(id = state.titleRes),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.padding(
top = 48.dp,
bottom = 16.dp,
),
)
Text(
text = stringResource(id = state.messageRes),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@Composable
private fun IndicatorDots(
selectedIndexProvider: () -> Int,
totalCount: Int,
onDotClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
items(totalCount) { index ->
val color = animateColorAsState(
targetValue = if (index == selectedIndexProvider()) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
},
label = "dotColor",
)
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(color.value)
.clickable { onDotClick(index) },
)
}
}
}

View file

@ -0,0 +1,161 @@
package com.x8bit.bitwarden.ui.auth.feature.welcome
import android.os.Parcelable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/**
* Manages application state for the welcome screen.
*/
@HiltViewModel
class WelcomeViewModel @Inject constructor() :
BaseViewModel<WelcomeState, WelcomeEvent, WelcomeAction>(
initialState = WelcomeState(
index = 0,
pages = listOf(
WelcomeState.WelcomeCard.CardOne,
WelcomeState.WelcomeCard.CardTwo,
WelcomeState.WelcomeCard.CardThree,
WelcomeState.WelcomeCard.CardFour,
),
),
) {
override fun handleAction(action: WelcomeAction) {
when (action) {
is WelcomeAction.PagerSwipe -> handlePagerSwipe(action)
is WelcomeAction.DotClick -> handleDotClick(action)
WelcomeAction.CreateAccountClick -> handleCreateAccountClick()
WelcomeAction.LoginClick -> handleLoginClick()
}
}
private fun handlePagerSwipe(action: WelcomeAction.PagerSwipe) {
mutableStateFlow.update { it.copy(index = action.index) }
}
private fun handleDotClick(action: WelcomeAction.DotClick) {
mutableStateFlow.update { it.copy(index = action.index) }
sendEvent(WelcomeEvent.UpdatePager(index = action.index))
}
private fun handleCreateAccountClick() {
sendEvent(WelcomeEvent.NavigateToCreateAccount)
}
private fun handleLoginClick() {
sendEvent(WelcomeEvent.NavigateToLogin)
}
}
/**
* Models state of the welcome screen.
*/
@Parcelize
data class WelcomeState(
val index: Int,
val pages: List<WelcomeCard>,
) : Parcelable {
/**
* A sealed class to represent the different cards the user can view on the welcome screen.
*/
sealed class WelcomeCard : Parcelable {
abstract val imageRes: Int
abstract val titleRes: Int
abstract val messageRes: Int
/**
* Represents the first card the user should see on the welcome screen.
*/
@Parcelize
data object CardOne : WelcomeCard() {
override val imageRes: Int = R.drawable.welcome_1
override val titleRes: Int = R.string.privacy_prioritized
override val messageRes: Int = R.string.welcome_message_1
}
/**
* Represents the second card the user should see on the welcome screen.
*/
@Parcelize
data object CardTwo : WelcomeCard() {
override val imageRes: Int = R.drawable.welcome_2
override val titleRes: Int = R.string.never_guess_again
override val messageRes: Int = R.string.welcome_message_2
}
/**
* Represents the third card the user should see on the welcome screen.
*/
@Parcelize
data object CardThree : WelcomeCard() {
override val imageRes: Int = R.drawable.welcome_3
override val titleRes: Int = R.string.level_up_your_logins
override val messageRes: Int = R.string.welcome_message_3
}
/**
* Represents the fourth card the user should see on the welcome screen.
*/
@Parcelize
data object CardFour : WelcomeCard() {
override val imageRes: Int = R.drawable.welcome_4
override val titleRes: Int = R.string.your_data_when_and_where_you_need_it
override val messageRes: Int = R.string.welcome_message_4
}
}
}
/**
* Models events for the welcome screen.
*/
sealed class WelcomeEvent {
/**
* Updates the current index of the pager.
*/
data class UpdatePager(
val index: Int,
) : WelcomeEvent()
/**
* Navigates to the create account screen.
*/
data object NavigateToCreateAccount : WelcomeEvent()
/**
* Navigates to the login screen.
*/
data object NavigateToLogin : WelcomeEvent()
}
/**
* Models actions for the welcome screen.
*/
sealed class WelcomeAction {
/**
* Swipe the pager to the given [index].
*/
data class PagerSwipe(
val index: Int,
) : WelcomeAction()
/**
* Click one of the page indicator dots at the given [index].
*/
data class DotClick(
val index: Int,
) : WelcomeAction()
/**
* Click the create account button.
*/
data object CreateAccountClick : WelcomeAction()
/**
* Click the login button.
*/
data object LoginClick : WelcomeAction()
}

View file

@ -29,6 +29,7 @@ import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.trustedDeviceGraph
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
import com.x8bit.bitwarden.ui.auth.feature.welcome.navigateToWelcome
import com.x8bit.bitwarden.ui.platform.feature.rootnav.util.toVaultItemListingType import com.x8bit.bitwarden.ui.platform.feature.rootnav.util.toVaultItemListingType
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.navigateToLoginApproval import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.navigateToLoginApproval
import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE
@ -85,6 +86,7 @@ fun RootNavScreen(
val targetRoute = when (state) { val targetRoute = when (state) {
RootNavState.Auth -> AUTH_GRAPH_ROUTE RootNavState.Auth -> AUTH_GRAPH_ROUTE
RootNavState.AuthWithWelcome -> AUTH_GRAPH_ROUTE
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
RootNavState.SetPassword -> SET_PASSWORD_ROUTE RootNavState.SetPassword -> SET_PASSWORD_ROUTE
RootNavState.Splash -> SPLASH_ROUTE RootNavState.Splash -> SPLASH_ROUTE
@ -133,6 +135,7 @@ fun RootNavScreen(
LaunchedEffect(state) { LaunchedEffect(state) {
when (val currentState = state) { when (val currentState = state) {
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions) RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
RootNavState.AuthWithWelcome -> navController.navigateToWelcome(rootNavOptions)
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions) RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions) RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions) RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)

View file

@ -25,7 +25,7 @@ import javax.inject.Inject
*/ */
@HiltViewModel @HiltViewModel
class RootNavViewModel @Inject constructor( class RootNavViewModel @Inject constructor(
authRepository: AuthRepository, private val authRepository: AuthRepository,
specialCircumstanceManager: SpecialCircumstanceManager, specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<RootNavState, Unit, RootNavAction>( ) : BaseViewModel<RootNavState, Unit, RootNavAction>(
initialState = RootNavState.Splash, initialState = RootNavState.Splash,
@ -52,7 +52,7 @@ class RootNavViewModel @Inject constructor(
} }
} }
@Suppress("CyclomaticComplexMethod", "MaxLineLength") @Suppress("CyclomaticComplexMethod", "MaxLineLength", "LongMethod")
private fun handleUserStateUpdateReceive( private fun handleUserStateUpdateReceive(
action: RootNavAction.Internal.UserStateUpdateReceive, action: RootNavAction.Internal.UserStateUpdateReceive,
) { ) {
@ -68,7 +68,13 @@ class RootNavViewModel @Inject constructor(
userState == null || userState == null ||
!userState.activeAccount.isLoggedIn || !userState.activeAccount.isLoggedIn ||
userState.hasPendingAccountAddition -> RootNavState.Auth userState.hasPendingAccountAddition -> {
if (authRepository.showWelcomeCarousel) {
RootNavState.AuthWithWelcome
} else {
RootNavState.Auth
}
}
userState.activeAccount.isVaultUnlocked -> { userState.activeAccount.isVaultUnlocked -> {
when (val specialCircumstance = action.specialCircumstance) { when (val specialCircumstance = action.specialCircumstance) {
@ -135,6 +141,12 @@ sealed class RootNavState : Parcelable {
@Parcelize @Parcelize
data object Auth : RootNavState() data object Auth : RootNavState()
/**
* App should show auth nav graph starting with the welcome carousel.
*/
@Parcelize
data object AuthWithWelcome : RootNavState()
/** /**
* App should show reset password graph. * App should show reset password graph.
*/ */

View file

@ -0,0 +1,73 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:pathData="M118.39,69.32H79.67C74.33,69.32 70,73.65 70,78.99V106.32C70,111.66 74.33,115.99 79.67,115.99H118.39C123.73,115.99 128.06,111.66 128.06,106.32V78.99C128.06,73.65 123.73,69.32 118.39,69.32ZM79.67,65.32C72.12,65.32 66,71.44 66,78.99V106.32C66,113.87 72.12,119.99 79.67,119.99H118.39C125.94,119.99 132.06,113.87 132.06,106.32V78.99C132.06,71.44 125.94,65.32 118.39,65.32H79.67Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M114,65V61C114,52.72 107.29,46 99,46C90.72,46 84,52.72 84,61V65H114ZM118,69H80V61C80,50.51 88.51,42 99,42C109.49,42 118,50.51 118,61V69Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M99,89.93C100.11,89.93 101,90.83 101,91.93L101,99.93C101,101.03 100.11,101.93 99,101.93C97.9,101.93 97,101.03 97,99.93L97,91.93C97,90.83 97.9,89.93 99,89.93Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M99,93.4C96.79,93.4 95,91.61 95,89.4V89.4C95,87.19 96.79,85.4 99,85.4C101.21,85.4 103,87.19 103,89.4V89.4C103,91.61 101.21,93.4 99,93.4Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M121.3,173C122.21,172.38 123.46,172.62 124.08,173.53C124.89,174.74 125.39,176.07 125.39,177.46C125.39,178.86 124.88,180.16 123.91,181.21C123.04,182.21 121.73,182.84 120.47,183.07C119.94,185.14 118.71,186.41 117.56,187.03C116.95,187.42 116.28,187.68 115.59,187.81C115.35,188.42 115.01,189 114.57,189.52C113.52,191.11 111.71,191.79 110.18,191.82C109.47,192.75 108.46,193.42 107.35,193.88C107.26,193.92 107.17,193.95 107.07,193.97C106.43,194.13 105.92,194.13 105.53,194.13L105.49,194.13C104.09,194.13 102.65,193.63 101.5,193.1C100.3,192.54 99.19,191.86 98.36,191.32C97.44,190.71 97.19,189.47 97.8,188.55C98.41,187.63 99.65,187.38 100.57,187.99C101.3,188.47 102.23,189.03 103.17,189.46C104.15,189.91 104.96,190.13 105.49,190.13C105.7,190.13 105.8,190.13 105.89,190.13C105.92,190.12 105.95,190.12 105.98,190.12C106.85,189.72 107.1,189.31 107.17,189.08C107.33,188.54 107.71,188.09 108.22,187.85C108.73,187.6 109.32,187.59 109.84,187.8C109.86,187.81 109.88,187.82 109.9,187.82C109.9,187.82 109.91,187.82 109.91,187.82C109.95,187.82 109.99,187.82 110.07,187.82C110.65,187.82 111.09,187.54 111.24,187.29C111.31,187.18 111.39,187.07 111.48,186.97C111.8,186.62 111.95,186.23 111.96,185.79C111.98,185.21 112.25,184.67 112.7,184.31C113.15,183.94 113.73,183.79 114.3,183.88C114.35,183.89 114.39,183.9 114.44,183.9C114.48,183.9 114.51,183.91 114.55,183.91C114.87,183.91 115.18,183.82 115.43,183.64C115.5,183.6 115.57,183.55 115.65,183.52C115.84,183.42 116.68,182.87 116.73,181.07C116.75,180.49 117.02,179.95 117.47,179.58C117.92,179.22 118.5,179.06 119.07,179.16C119.15,179.18 119.24,179.18 119.34,179.18C119.96,179.18 120.66,178.86 120.91,178.57C120.92,178.56 120.92,178.56 120.93,178.55C120.94,178.54 120.94,178.53 120.95,178.52C121.27,178.18 121.39,177.83 121.39,177.46C121.39,177.05 121.25,176.49 120.77,175.78C120.15,174.87 120.39,173.62 121.3,173Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M95.47,149.98C93.74,151.13 91.78,153.18 89.9,156.83L89.89,156.85L89.88,156.88C89.67,157.26 89.64,157.81 89.96,158.41C90.27,158.91 91.25,159.48 92.89,158.63L92.89,158.63C94.91,157.58 96.42,156.48 97.28,155.85C97.42,155.75 97.54,155.66 97.65,155.58L98.17,155.21H98.81C100.21,155.21 101,155.12 101.47,155.04L103.03,154.78L103.64,156.24C104.01,157.1 104.64,158.5 106.01,159.92C107.37,161.33 109.54,162.84 113.08,163.75L113.58,163.88L122.96,172.74L135.57,165.65L126.23,149.61L115.19,154.62L102.52,149.03C102.49,149.02 102.46,149.01 102.44,149C102.43,149 102.36,148.98 102.31,148.96C102.28,148.95 102.26,148.94 102.25,148.93C102.12,148.89 101.96,148.83 101.75,148.78C101.34,148.66 100.74,148.55 100,148.55C98.76,148.55 97.18,148.85 95.47,149.98ZM93.26,146.65C95.69,145.04 98.06,144.55 100,144.55C101.17,144.55 102.12,144.73 102.8,144.92C103.1,145 103.35,145.08 103.53,145.15H103.62L115.17,150.24L125.32,145.63C126.15,145.24 127.02,145.35 127.6,145.55C128.38,145.81 128.98,146.34 129.32,146.97L139.65,164.71L139.71,164.84C140.3,166.33 139.64,167.74 138.65,168.48L138.55,168.56L124.21,176.62L124.17,176.65C123.93,176.76 123.67,176.86 123.35,176.92C123.1,176.96 122.87,176.96 122.79,176.96C122.78,176.96 122.78,176.96 122.77,176.96C122.09,176.96 121.28,176.68 120.69,176.1L111.58,167.49C107.62,166.38 104.94,164.58 103.13,162.7C101.91,161.43 101.12,160.17 100.59,159.16C100.26,159.18 99.89,159.2 99.47,159.21C98.56,159.87 96.89,161.06 94.74,162.18C91.81,163.7 88.2,163.31 86.5,160.42L86.48,160.39L86.47,160.37C85.61,158.79 85.38,156.78 86.35,154.98C88.47,150.88 90.84,148.25 93.26,146.65Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M76.54,175.07C77.74,173.63 79.32,172.48 81.46,172.65L81.47,172.65L81.48,172.65C82.74,172.78 83.77,173.33 84.53,173.93C84.99,173.74 85.46,173.59 85.93,173.48C86.77,173.3 87.74,173.25 88.64,173.55L88.66,173.56L88.67,173.56C89.99,174.02 90.88,174.95 91.48,175.84C91.62,175.84 91.76,175.85 91.91,175.86C94.43,176.1 95.89,177.52 96.69,178.74C98.02,179.07 99,179.82 99.67,180.74C100.43,181.78 100.73,182.95 100.88,183.59C101.36,185.4 101.56,188.43 100.89,191.34L100.87,191.41L100.85,191.48C100.15,193.76 98.07,195 95.83,195C94.1,195 92.28,194.36 90.91,193.16C88.54,192.93 86.52,192.12 85.17,190.48C83.82,190.44 82.36,190 81.04,188.95L81,188.91L80.96,188.88C80.33,188.31 79.67,187.51 79.21,186.4C78.68,186.18 78.07,185.88 77.48,185.48C76.45,184.79 75.32,183.71 74.78,182.1C73.99,179.73 74.76,177.33 76.51,175.11L76.53,175.09L76.54,175.07ZM79.63,177.61C78.39,179.2 78.36,180.2 78.57,180.83C78.74,181.32 79.12,181.77 79.71,182.16C80.29,182.55 80.94,182.8 81.37,182.92L82.49,183.25L82.76,184.38C82.93,185.13 83.28,185.57 83.58,185.85C84.19,186.32 84.82,186.49 85.4,186.49C85.54,186.49 85.66,186.47 85.76,186.45L87.29,186.11L87.98,187.51C88.02,187.58 88.06,187.65 88.1,187.72C88.52,188.36 89.34,188.96 91.13,189.17L92.26,188.63L93.25,189.85C93.8,190.52 94.83,191 95.83,191C96.57,191 96.89,190.67 97.01,190.36C97.51,188.1 97.32,185.77 97.01,184.59L97,184.56L96.99,184.52C96.86,183.95 96.7,183.46 96.44,183.11C96.26,182.85 95.98,182.6 95.29,182.56L94.05,182.49L93.56,181.35C93.31,180.75 92.76,179.96 91.53,179.85C91.33,179.83 91.15,179.85 90.98,179.91L89.23,180.46L88.53,178.77C88.39,178.43 88.2,178.09 87.97,177.82C87.76,177.56 87.55,177.41 87.37,177.35C87.34,177.34 87.16,177.31 86.78,177.39C86.4,177.47 85.94,177.65 85.47,177.94C85.42,177.97 85.38,178 85.34,178.03L83.83,179.08L82.66,177.67C82.44,177.41 82.18,177.15 81.89,176.95C81.61,176.76 81.35,176.66 81.11,176.64C80.84,176.62 80.42,176.68 79.63,177.61Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M86.12,175.89C86.7,176.83 86.41,178.06 85.47,178.64C84.93,178.97 83.68,179.84 83.35,181.2C83.13,182.12 83.09,182.84 83.12,183.34C83.15,183.8 83.22,184.01 83.22,184.01C83.22,184.01 83.21,184.01 83.21,183.99C83.69,184.99 83.28,186.18 82.28,186.66C81.29,187.15 80.09,186.73 79.61,185.74C79.12,184.74 78.86,182.76 79.46,180.26C80.21,177.21 82.78,175.6 83.36,175.24C84.31,174.66 85.54,174.95 86.12,175.89Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M92.14,177.35C92.64,178.33 92.26,179.53 91.28,180.04C90.88,180.24 89.4,181.38 88.76,183.56C88.11,185.8 88.52,187.03 88.57,187.15C88.57,187.16 88.57,187.16 88.57,187.16C89.13,188.12 88.8,189.34 87.85,189.9C86.9,190.45 85.67,190.13 85.11,189.18C84.43,188 84.01,185.56 84.92,182.44C85.89,179.1 88.17,177.14 89.45,176.48C90.43,175.98 91.63,176.36 92.14,177.35Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M96.43,180.65C96.94,181.63 96.55,182.84 95.57,183.34C95.57,183.34 95.57,183.34 95.56,183.35C95.52,183.39 95.28,183.58 94.96,184.07C94.62,184.58 94.28,185.28 94.05,186.09C93.83,186.82 93.76,187.7 93.78,188.47C93.8,188.85 93.83,189.16 93.87,189.39C93.89,189.5 93.91,189.56 93.92,189.59C94.38,190.52 94.05,191.68 93.13,192.21C92.18,192.77 90.96,192.44 90.4,191.49C90.13,191.03 90,190.48 89.94,190.1C89.85,189.65 89.8,189.14 89.78,188.6C89.75,187.53 89.84,186.21 90.21,184.97C90.55,183.79 91.06,182.72 91.62,181.86C92.15,181.06 92.87,180.23 93.74,179.79C94.72,179.28 95.93,179.67 96.43,180.65Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M102.95,182.08C102.95,182.08 102.95,182.08 102.95,182.08L102.96,182.09L103,182.12C103.03,182.15 103.08,182.2 103.14,182.25C103.27,182.37 103.46,182.54 103.69,182.74C104.15,183.16 104.79,183.72 105.52,184.32C107.01,185.57 108.67,186.83 109.75,187.37C110.74,187.87 111.14,189.07 110.65,190.05C110.15,191.04 108.95,191.44 107.97,190.95C106.41,190.17 104.43,188.62 102.96,187.39C102.19,186.76 101.51,186.16 101.03,185.73C100.78,185.51 100.59,185.33 100.45,185.21C100.38,185.15 100.33,185.1 100.29,185.06L100.25,185.02L100.23,185.01C100.23,185.01 100.23,185.01 101.59,183.54L100.23,185.01C99.42,184.26 99.38,182.99 100.13,182.18C100.88,181.37 102.14,181.33 102.95,182.08Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M102.07,176.94C102.81,176.11 104.07,176.03 104.9,176.76C104.9,176.76 104.9,176.76 104.9,176.76L104.91,176.77L104.96,176.82C105.01,176.85 105.07,176.91 105.16,176.99C105.34,177.14 105.59,177.36 105.91,177.63C106.54,178.16 107.43,178.9 108.42,179.68C110.46,181.29 112.77,182.97 114.34,183.72C115.34,184.19 115.76,185.39 115.29,186.38C114.82,187.38 113.63,187.81 112.63,187.33C110.6,186.37 107.95,184.41 105.94,182.82C104.9,182 103.98,181.24 103.32,180.68C103,180.4 102.73,180.17 102.54,180.01C102.45,179.93 102.38,179.87 102.33,179.83L102.27,179.78L102.25,179.76C102.25,179.76 102.25,179.76 103.57,178.26L102.25,179.76C101.42,179.03 101.34,177.76 102.07,176.94Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M106.01,171.68C106.73,170.84 107.99,170.73 108.83,171.45C108.83,171.45 108.83,171.45 108.83,171.45L108.85,171.46L108.91,171.51C108.96,171.55 109.04,171.62 109.13,171.7C109.33,171.87 109.62,172.11 109.98,172.4C110.7,172.99 111.71,173.79 112.83,174.65C115.14,176.4 117.79,178.25 119.6,179.08C120.61,179.54 121.05,180.73 120.59,181.73C120.13,182.74 118.94,183.18 117.94,182.72C115.67,181.68 112.7,179.57 110.41,177.83C109.24,176.94 108.2,176.11 107.46,175.5C107.08,175.2 106.78,174.95 106.57,174.77C106.47,174.69 106.39,174.62 106.33,174.57L106.27,174.52L106.25,174.5C106.25,174.5 106.25,174.5 107.54,172.97L106.25,174.5C105.4,173.79 105.3,172.52 106.01,171.68Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M59,148h34.46v31.9h-34.46z"/>
<path
android:pathData="M72.97,152.33C75.4,153.57 77.48,154.21 78.98,154.68H79C80.05,155.01 80.6,155.19 80.94,155.37C81.43,155.63 82,155.68 82.52,155.5C82.76,155.42 82.97,155.32 83.09,155.26C83.09,155.26 83.1,155.26 83.11,155.26L93.46,150.03C93.31,148.94 92.31,148.17 91.21,148.31C87.87,148.76 84.19,150.4 82.18,151.33L80.18,150.87C78.52,150.36 76.44,149.71 73.98,148.35C73.96,148.34 73.94,148.33 73.91,148.32C72.65,147.69 70.88,148.01 70,149.48L59.44,167.56C58.68,168.84 58.84,170.73 60.43,171.69C60.47,171.71 60.5,171.73 60.54,171.75L75.78,179.67C76.76,180.18 77.97,179.8 78.48,178.82C78.99,177.84 78.61,176.63 77.63,176.12L63.39,168.73L72.97,152.33Z"
android:fillColor="#020F66"/>
</group>
<path
android:pathData="M180.1,64.02C185.31,81.07 184.91,99.34 178.94,116.14C172.98,132.94 161.76,147.39 146.95,157.37C146.03,157.99 144.79,157.75 144.17,156.83C143.55,155.92 143.8,154.67 144.71,154.06C158.82,144.55 169.49,130.79 175.17,114.8C180.85,98.81 181.24,81.42 176.27,65.19C171.31,48.97 161.25,34.75 147.58,24.64C133.91,14.52 117.34,9.04 100.32,9C83.29,8.96 66.7,14.36 52.98,24.41C39.26,34.45 29.13,48.62 24.09,64.82C19.04,81.03 19.34,98.41 24.94,114.43C30.54,130.46 41.15,144.27 55.21,153.84C56.12,154.46 56.36,155.7 55.74,156.62C55.12,157.53 53.87,157.77 52.96,157.14C38.19,147.09 27.05,132.59 21.17,115.75C15.28,98.92 14.97,80.66 20.27,63.63C25.57,46.61 36.2,31.73 50.61,21.18C65.02,10.63 82.45,4.96 100.33,5C118.21,5.04 135.6,10.8 149.96,21.42C164.32,32.04 174.88,46.97 180.1,64.02Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M172.33,66.32C177.03,81.68 176.66,98.14 171.29,113.28C165.91,128.42 155.8,141.44 142.45,150.43C141.99,150.74 141.37,150.62 141.06,150.16C140.75,149.7 140.87,149.08 141.33,148.77C154.33,140.02 164.17,127.34 169.4,112.61C174.63,97.87 174.99,81.85 170.41,66.9C165.84,51.95 156.57,38.85 143.98,29.54C131.38,20.22 116.11,15.17 100.43,15.13C84.74,15.09 69.45,20.07 56.81,29.32C44.16,38.58 34.83,51.63 30.18,66.56C25.53,81.49 25.81,97.51 30.97,112.27C36.13,127.03 45.91,139.76 58.86,148.57C59.32,148.88 59.44,149.51 59.13,149.96C58.82,150.42 58.19,150.54 57.74,150.23C44.43,141.17 34.39,128.1 29.08,112.93C23.78,97.77 23.5,81.3 28.27,65.97C33.05,50.63 42.64,37.22 55.62,27.71C68.61,18.2 84.31,13.09 100.43,13.13C116.54,13.17 132.22,18.36 145.16,27.93C158.1,37.5 167.62,50.96 172.33,66.32Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,58 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:pathData="M21,16.62C21,9.65 26.65,4 33.62,4H93.31C100.29,4 105.94,9.65 105.94,16.62V41.26C105.94,42.36 105.04,43.26 103.94,43.26C102.83,43.26 101.94,42.36 101.94,41.26V16.62C101.94,11.86 98.08,8 93.31,8H33.62C28.86,8 25,11.86 25,16.62V146.52C25,151.29 28.86,155.14 33.62,155.14H93.59C94.7,155.14 95.59,156.04 95.59,157.14C95.59,158.25 94.7,159.14 93.59,159.14H33.62C26.65,159.14 21,153.49 21,146.52V16.62Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M59.42,22.21C59.42,21.11 60.31,20.21 61.42,20.21H65.51C66.61,20.21 67.51,21.11 67.51,22.21C67.51,23.32 66.61,24.21 65.51,24.21H61.42C60.31,24.21 59.42,23.32 59.42,22.21Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M105.7,44.86C100.94,44.86 97.08,48.71 97.08,53.48V183.38C97.08,188.14 100.94,192 105.7,192H165.38C170.14,192 174,188.14 174,183.38V53.48C174,48.71 170.14,44.86 165.38,44.86H105.7ZM93.08,53.48C93.08,46.5 98.73,40.86 105.7,40.86H165.38C172.35,40.86 178,46.5 178,53.48V183.38C178,190.35 172.35,196 165.38,196H105.7C98.73,196 93.08,190.35 93.08,183.38V53.48Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M130.14,59.07C130.14,57.97 131.04,57.07 132.14,57.07H136.41C137.51,57.07 138.41,57.97 138.41,59.07C138.41,60.18 137.51,61.07 136.41,61.07H132.14C131.04,61.07 130.14,60.18 130.14,59.07Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M77.46,58.89C77.46,57.79 78.35,56.89 79.46,56.89H83.24C85.55,56.89 87.6,58.71 87.6,61.14V64.73C87.6,65.84 86.7,66.73 85.6,66.73C84.49,66.73 83.6,65.84 83.6,64.73V61.14C83.6,61.11 83.59,61.06 83.52,61C83.46,60.94 83.36,60.89 83.24,60.89H79.46C78.35,60.89 77.46,60 77.46,58.89ZM36.42,61.15C36.42,58.71 38.45,56.91 40.77,56.91H44.55C45.66,56.91 46.55,57.8 46.55,58.91C46.55,60.01 45.66,60.91 44.55,60.91H40.77C40.63,60.91 40.54,60.96 40.48,61.01C40.43,61.06 40.42,61.11 40.42,61.15V64.75C40.42,65.85 39.52,66.75 38.42,66.75C37.31,66.75 36.42,65.85 36.42,64.75V61.15Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M38.42,103.91C39.52,103.91 40.42,104.8 40.42,105.91V109.49C40.42,109.51 40.43,109.57 40.49,109.63C40.55,109.69 40.65,109.73 40.77,109.73H44.55C45.66,109.73 46.55,110.63 46.55,111.73C46.55,112.84 45.66,113.73 44.55,113.73H40.77C38.46,113.73 36.42,111.91 36.42,109.49V105.91C36.42,104.8 37.31,103.91 38.42,103.91ZM85.6,103.91C86.7,103.91 87.6,104.8 87.6,105.91V109.49C87.6,111.9 85.57,113.73 83.24,113.73H79.46C78.35,113.73 77.46,112.84 77.46,111.73C77.46,110.63 78.35,109.73 79.46,109.73H83.24C83.37,109.73 83.47,109.68 83.53,109.63C83.58,109.57 83.6,109.52 83.6,109.49V105.91C83.6,104.8 84.49,103.91 85.6,103.91Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M62.15,70.01C57.23,70.01 53.38,72.96 53.38,76.96V79.51C53.38,79.51 53.38,79.52 53.38,79.52C53.38,79.62 53.39,80.05 53.18,80.51C53.08,80.75 52.95,80.93 52.87,81.02C52.82,81.08 52.77,81.15 52.74,81.18C52.71,81.21 52.68,81.24 52.65,81.28C52.64,81.3 52.62,81.32 52.61,81.33C52.55,81.4 52.48,81.48 52.4,81.58C52.37,81.62 52.36,81.64 52.35,81.66C52.34,81.67 52.33,81.7 52.33,81.76C52.33,81.78 52.32,81.8 52.32,81.82C52.31,81.94 52.32,82.21 52.53,82.79C53.3,84.95 54.78,87.61 56.59,89.73C58.44,91.88 60.41,93.21 62.15,93.21C63,93.21 63.97,92.89 64.97,92.26L64.97,92.25C65.98,91.62 66.98,90.72 67.93,89.64C69.69,87.61 71.03,85.14 71.52,83.17L71.09,80.86C71.05,80.79 71.01,80.73 70.98,80.66C70.88,80.46 70.81,80.25 70.78,80.03C70.78,80.02 70.78,80 70.77,79.98C70.76,79.93 70.76,79.87 70.75,79.8C70.75,79.73 70.74,79.66 70.74,79.58V78.87C70.74,78.77 70.75,78.68 70.76,78.59L70.7,76.76C70.43,74.49 69.48,72.84 68.07,71.74C66.63,70.62 64.62,70.01 62.15,70.01ZM70.77,79.78L70.77,79.78L70.77,79.78L70.77,79.79C70.77,79.79 70.77,79.78 70.77,79.78ZM75.47,83.81C75.53,83.64 75.56,83.46 75.57,83.28C75.69,82.52 75.73,81.77 75.62,81.07C75.53,80.45 75.34,79.94 75.1,79.52L74.69,76.38C74.32,73.13 72.9,70.43 70.53,68.59L70.53,68.59C68.2,66.77 65.24,66.01 62.15,66.01C56.06,66.01 49.38,69.84 49.38,76.96V78.96C49.35,78.99 49.32,79.02 49.3,79.05L49.3,79.06C48.85,79.61 48.43,80.39 48.34,81.42C48.24,82.39 48.47,83.31 48.76,84.13L48.76,84.13C49.69,86.75 51.42,89.84 53.55,92.33L53.55,92.33C55.56,94.67 58.56,97.21 62.15,97.21C63.98,97.21 65.68,96.54 67.1,95.64C68.53,94.75 69.82,93.54 70.94,92.27L70.94,92.27C73.08,89.81 74.86,86.65 75.47,83.81ZM49.38,79.52C49.38,79.52 49.38,79.52 49.38,79.52L49.38,79.52Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M56.68,91.31C57.92,91.17 59.05,92.07 59.18,93.32C59.29,94.28 59.33,95.8 59.14,97.39C58.96,98.93 58.52,100.84 57.44,102.34C56.71,103.36 55.29,103.59 54.28,102.86C53.26,102.13 53.02,100.71 53.76,99.69C54.16,99.12 54.48,98.12 54.63,96.85C54.78,95.64 54.74,94.46 54.67,93.81C54.53,92.56 55.43,91.44 56.68,91.31Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M68.12,91.31C69.37,91.44 70.27,92.56 70.13,93.81C70.06,94.46 70.02,95.64 70.17,96.85C70.32,98.12 70.64,99.12 71.04,99.69C71.78,100.71 71.54,102.13 70.52,102.86C69.51,103.59 68.09,103.36 67.36,102.34C66.28,100.84 65.84,98.93 65.66,97.39C65.47,95.8 65.51,94.28 65.62,93.32C65.75,92.07 66.88,91.17 68.12,91.31Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M104.11,118.55C104.11,110.43 110.72,103.86 118.83,103.86H152.25C160.39,103.86 166.98,110.45 166.98,118.55C166.98,126.68 160.37,133.25 152.25,133.25H118.83C110.7,133.25 104.11,126.66 104.11,118.55ZM118.83,107.86C112.91,107.86 108.11,112.65 108.11,118.55C108.11,124.44 112.9,129.25 118.83,129.25H152.25C158.18,129.25 162.98,124.46 162.98,118.55C162.98,112.66 158.19,107.86 152.25,107.86H118.83Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M119.37,122.22C121.4,122.22 123.04,120.58 123.04,118.55C123.04,116.53 121.4,114.89 119.37,114.89C117.33,114.89 115.69,116.53 115.69,118.55C115.69,120.58 117.33,122.22 119.37,122.22Z"
android:fillColor="#020F66"/>
<path
android:pathData="M129.66,122.22C131.7,122.22 133.34,120.58 133.34,118.55C133.34,116.53 131.7,114.89 129.66,114.89C127.63,114.89 125.99,116.53 125.99,118.55C125.99,120.58 127.63,122.22 129.66,122.22Z"
android:fillColor="#020F66"/>
<path
android:pathData="M139.98,122.22C142.01,122.22 143.66,120.58 143.66,118.55C143.66,116.53 142.01,114.89 139.98,114.89C137.95,114.89 136.3,116.53 136.3,118.55C136.3,120.58 137.95,122.22 139.98,122.22Z"
android:fillColor="#020F66"/>
<path
android:pathData="M150.27,122.22C152.3,122.22 153.95,120.58 153.95,118.55C153.95,116.53 152.3,114.89 150.27,114.89C148.24,114.89 146.59,116.53 146.59,118.55C146.59,120.58 148.24,122.22 150.27,122.22Z"
android:fillColor="#020F66"/>
</vector>

View file

@ -0,0 +1,46 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:pathData="M3.02,48.35C3.02,41.17 8.84,35.35 16.02,35.35H146.45C153.63,35.35 159.45,41.17 159.45,48.35V61.99H155.45V48.35C155.45,43.38 151.43,39.35 146.45,39.35H16.02C11.05,39.35 7.02,43.38 7.02,48.35V126.44C7.02,131.41 11.05,135.44 16.02,135.44H28.96V139.44H16.02C8.84,139.44 3.02,133.62 3.02,126.44V48.35ZM84.43,135.44H104.21V139.44H84.43V135.44Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M144.95,113.02C159.1,113.02 170.57,101.54 170.57,87.4C170.57,73.25 159.1,61.78 144.95,61.78C130.8,61.78 119.33,73.25 119.33,87.4C119.33,101.54 130.8,113.02 144.95,113.02ZM144.95,117.02C161.31,117.02 174.57,103.75 174.57,87.4C174.57,71.04 161.31,57.78 144.95,57.78C128.59,57.78 115.33,71.04 115.33,87.4C115.33,103.75 128.59,117.02 144.95,117.02Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M57.11,90.42C48.92,90.42 41.75,98.35 41.75,108.85H37.75C37.75,96.78 46.13,86.42 57.11,86.42C68.1,86.42 76.48,96.78 76.48,108.85H72.48C72.48,98.35 65.31,90.42 57.11,90.42Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M100.39,157.96H187.48C189.57,157.96 190.38,157.41 190.62,157.16C190.76,157.02 191.18,156.51 190.92,154.91C187.44,133.48 168.25,116.93 144.93,116.93C121.61,116.93 102.43,133.48 98.95,154.91C98.81,155.74 98.99,156.7 99.36,157.33C99.53,157.61 99.69,157.75 99.81,157.82C99.9,157.88 100.06,157.96 100.39,157.96ZM100.39,161.96H187.48C192.85,161.96 195.67,159.19 194.87,154.27C191.06,130.84 170.15,112.93 144.93,112.93C119.71,112.93 98.81,130.84 95,154.27C94.46,157.6 96.19,161.96 100.39,161.96Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M74.73,110.8H39.49C35.07,110.8 31.49,114.38 31.49,118.8V143.83C31.49,148.25 35.07,151.83 39.49,151.83H74.73C79.15,151.83 82.73,148.25 82.73,143.83V118.8C82.73,114.38 79.15,110.8 74.73,110.8ZM39.49,106.8C32.86,106.8 27.49,112.18 27.49,118.8V143.83C27.49,150.46 32.86,155.83 39.49,155.83H74.73C81.36,155.83 86.73,150.46 86.73,143.83V118.8C86.73,112.18 81.36,106.8 74.73,106.8H39.49Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M57.11,131.36C58.22,131.36 59.11,132.25 59.11,133.36L59.11,140.53C59.11,141.64 58.22,142.53 57.11,142.53C56.01,142.53 55.11,141.64 55.11,140.53L55.11,133.36C55.11,132.25 56.01,131.36 57.11,131.36Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M156.18,52.49H4V50.49H156.18V52.49Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M61.6,129.82C61.6,132.33 59.57,134.36 57.06,134.36C54.54,134.36 52.51,132.33 52.51,129.82C52.51,127.3 54.54,125.27 57.06,125.27C59.57,125.27 61.6,127.3 61.6,129.82Z"
android:fillColor="#10949D"/>
<path
android:pathData="M131,45C131,46.1 130.1,47 129,47C127.89,47 127,46.1 127,45C127,43.9 127.89,43 129,43C130.1,43 131,43.9 131,45Z"
android:fillColor="#020F66"/>
<path
android:pathData="M140,45C140,46.1 139.1,47 138,47C136.9,47 136,46.1 136,45C136,43.9 136.9,43 138,43C139.1,43 140,43.9 140,45Z"
android:fillColor="#020F66"/>
<path
android:pathData="M149,45C149,46.1 148.1,47 147,47C145.9,47 145,46.1 145,45C145,43.9 145.9,43 147,43C148.1,43 149,43.9 149,45Z"
android:fillColor="#020F66"/>
</vector>

View file

@ -0,0 +1,56 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<group>
<clip-path
android:pathData="M0,0h200v200h-200z"/>
<path
android:pathData="M28.49,54.09C28.49,50.04 31.77,46.77 35.81,46.77H155.87C159.91,46.77 163.19,50.04 163.19,54.09V59.35H161.19V54.09C161.19,51.15 158.81,48.77 155.87,48.77H35.81C32.87,48.77 30.49,51.15 30.49,54.09V69.88H28.49V54.09ZM44.23,124.7L125.33,124.7V126.7L44.23,126.7V124.7Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M62.24,153.09C62.24,151.98 63.14,151.09 64.24,151.09H125.33C126.43,151.09 127.33,151.98 127.33,153.09C127.33,154.19 126.43,155.09 125.33,155.09H64.24C63.14,155.09 62.24,154.19 62.24,153.09Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M83.04,153.09V134.38H87.04V153.09H83.04Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M104.07,153.09V134.38H108.07V153.09H104.07Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M20.12,53.03C20.12,44.95 26.67,38.39 34.76,38.39H156.93C165.01,38.39 171.56,44.95 171.56,53.03V59.35H167.56V53.03C167.56,47.16 162.8,42.39 156.93,42.39H34.76C28.88,42.39 24.12,47.16 24.12,53.03V69.88H20.12V53.03ZM43.71,131.07H125.33V135.07H43.71V131.07Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M95.84,69.83C91.6,69.83 88.41,72.77 88.41,76.06V81.47H86.41V76.06C86.41,71.39 90.8,67.83 95.84,67.83C100.88,67.83 105.27,71.37 105.27,76.06V81.47H103.27V76.06C103.27,72.75 100.08,69.83 95.84,69.83Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M82.2,87.87C82.2,83.83 85.48,80.55 89.52,80.55H102.16C106.2,80.55 109.48,83.83 109.48,87.87V96.29C109.48,100.34 106.2,103.61 102.16,103.61H89.52C85.48,103.61 82.2,100.34 82.2,96.29V87.87ZM89.52,82.55C86.58,82.55 84.2,84.93 84.2,87.87V96.29C84.2,99.23 86.58,101.61 89.52,101.61H102.16C105.1,101.61 107.48,99.23 107.48,96.29V87.87C107.48,84.93 105.1,82.55 102.16,82.55H89.52Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M95,91.54C94.52,91.28 94.21,90.8 94.21,90.25L94.21,90C94.21,89.18 94.91,88.51 95.79,88.51C96.66,88.51 97.37,89.18 97.37,90V90.25C97.37,90.8 97.05,91.28 96.58,91.54L96.58,94.5C96.58,94.91 96.22,95.25 95.79,95.25C95.35,95.25 95,94.91 95,94.5L95,91.54Z"
android:fillColor="#10949D"/>
<path
android:pathData="M35.81,71.78H10.53C6.92,71.78 4,74.7 4,78.31V141.5C4,145.11 6.92,148.03 10.53,148.03H35.81C39.42,148.03 42.34,145.11 42.34,141.5V78.31C42.34,74.7 39.42,71.78 35.81,71.78ZM10.53,67.78C4.72,67.78 0,72.49 0,78.31V141.5C0,147.32 4.72,152.03 10.53,152.03H35.81C41.63,152.03 46.34,147.32 46.34,141.5V78.31C46.34,72.49 41.63,67.78 35.81,67.78H10.53Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M21.45,77.05C21.45,76.47 21.93,76 22.51,76H23.82C24.4,76 24.87,76.47 24.87,77.05C24.87,77.64 24.4,78.11 23.82,78.11H22.51C21.93,78.11 21.45,77.64 21.45,77.05Z"
android:fillColor="#10949D"
android:fillType="evenOdd"/>
<path
android:pathData="M123.33,67.78C123.33,62.02 128,57.35 133.76,57.35H189.57C195.33,57.35 200,62.02 200,67.78V152.03C200,157.79 195.33,162.46 189.57,162.46H133.76C128,162.46 123.33,157.79 123.33,152.03V67.78ZM133.76,61.35C130.21,61.35 127.33,64.23 127.33,67.78V152.03C127.33,155.58 130.21,158.46 133.76,158.46H189.57C193.12,158.46 196,155.58 196,152.03V67.78C196,64.23 193.12,61.35 189.57,61.35H133.76Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M164.3,153.09C164.3,154.25 163.35,155.19 162.19,155.19C161.03,155.19 160.09,154.25 160.09,153.09C160.09,151.92 161.03,150.98 162.19,150.98C163.35,150.98 164.3,151.92 164.3,153.09Z"
android:fillColor="#10949D"/>
</group>
</vector>

View file

@ -927,4 +927,12 @@ Do you want to switch to this account?</string>
<string name="self_hosted_server_url">Self-hosted server URL</string> <string name="self_hosted_server_url">Self-hosted server URL</string>
<string name="passkey_operation_failed_because_user_could_not_be_verified">Passkey operation failed because user could not be verified.</string> <string name="passkey_operation_failed_because_user_could_not_be_verified">Passkey operation failed because user could not be verified.</string>
<string name="user_verification_direction">User verification</string> <string name="user_verification_direction">User verification</string>
<string name="privacy_prioritized">Privacy, prioritized</string>
<string name="welcome_message_1">Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect whats important to you.</string>
<string name="never_guess_again">Never guess again</string>
<string name="welcome_message_2">Set up biometric unlock and autofill to log into your accounts without typing a single letter.</string>
<string name="level_up_your_logins">Level up your logins</string>
<string name="welcome_message_3">Use the generator to create and save strong, unique passwords for all your accounts.</string>
<string name="your_data_when_and_where_you_need_it">Your data, when and where you need it</string>
<string name="welcome_message_4">Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps.</string>
</resources> </resources>

View file

@ -84,9 +84,11 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -217,6 +219,7 @@ class AuthRepositoryTest {
getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD) getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD)
} returns mutableActivePolicyFlow } returns mutableActivePolicyFlow
} }
private val featureFlagManager: FeatureFlagManager = mockk()
private val repository = AuthRepositoryImpl( private val repository = AuthRepositoryImpl(
accountsService = accountsService, accountsService = accountsService,
@ -236,6 +239,7 @@ class AuthRepositoryTest {
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
pushManager = pushManager, pushManager = pushManager,
policyManager = policyManager, policyManager = policyManager,
featureFlagManager = featureFlagManager,
) )
@BeforeEach @BeforeEach
@ -4504,6 +4508,22 @@ class AuthRepositoryTest {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `showWelcomeCarousel should return value from settings repository and feature flag manager`() {
every { settingsRepository.hasUserLoggedInOrCreatedAccount } returns false
every { featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel) } returns true
assertTrue(repository.showWelcomeCarousel)
every { settingsRepository.hasUserLoggedInOrCreatedAccount } returns true
every { featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel) } returns true
assertFalse(repository.showWelcomeCarousel)
every { settingsRepository.hasUserLoggedInOrCreatedAccount } returns true
every { featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel) } returns false
assertFalse(repository.showWelcomeCarousel)
}
@Test @Test
fun `getOrganizationDomainSsoDetails Failure should return Failure `() = runTest { fun `getOrganizationDomainSsoDetails Failure should return Failure `() = runTest {
val email = "test@gmail.com" val email = "test@gmail.com"

View file

@ -0,0 +1,124 @@
package com.x8bit.bitwarden.ui.auth.feature.welcome
import androidx.compose.ui.test.assertIsDisplayed
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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertTrue
import org.robolectric.annotation.Config
class WelcomeScreenTest : BaseComposeTest() {
private var onNavigateToCreateAccountCalled = false
private var onNavigateToLoginCalled = false
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<WelcomeEvent>()
private val viewModel = mockk<WelcomeViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
}
@Before
fun setUp() {
composeTestRule.setContent {
WelcomeScreen(
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
onNavigateToLogin = { onNavigateToLoginCalled = true },
viewModel = viewModel,
)
}
}
@Test
fun `pages should display and update according to state`() {
composeTestRule
.onNodeWithText("Privacy, prioritized")
.assertExists()
.assertIsDisplayed()
mutableEventFlow.tryEmit(WelcomeEvent.UpdatePager(index = 1))
composeTestRule
.onNodeWithText("Privacy, prioritized")
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Never guess again")
.assertExists()
.assertIsDisplayed()
mutableStateFlow.update { it.copy(pages = listOf(WelcomeState.WelcomeCard.CardThree)) }
composeTestRule
.onNodeWithText("Level up your logins")
.assertExists()
.assertIsDisplayed()
}
@Config(qualifiers = "land")
@Test
fun `pages should display and update according to state in landscape mode`() {
composeTestRule
.onNodeWithText("Privacy, prioritized")
.assertExists()
.assertIsDisplayed()
mutableEventFlow.tryEmit(WelcomeEvent.UpdatePager(index = 1))
composeTestRule
.onNodeWithText("Privacy, prioritized")
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Never guess again")
.assertExists()
.assertIsDisplayed()
mutableStateFlow.update { it.copy(pages = listOf(WelcomeState.WelcomeCard.CardThree)) }
composeTestRule
.onNodeWithText("Level up your logins")
.assertExists()
.assertIsDisplayed()
}
@Test
fun `NavigateToCreateAccount event should call onNavigateToCreateAccount`() {
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToCreateAccount)
assertTrue(onNavigateToCreateAccountCalled)
}
@Test
fun `NavigateToLogin event should call onNavigateToLogin`() {
mutableEventFlow.tryEmit(WelcomeEvent.NavigateToLogin)
assertTrue(onNavigateToLoginCalled)
}
@Test
fun `create account button click should send CreateAccountClick action`() {
composeTestRule
.onNodeWithText("Create account")
.performClick()
verify { viewModel.trySendAction(WelcomeAction.CreateAccountClick) }
}
@Test
fun `login button click should send LoginClick action`() {
// Use an empty list of pages to guarantee that the login button
// will be in view on the UI testing viewport.
mutableStateFlow.update { it.copy(pages = emptyList()) }
composeTestRule
.onNodeWithText("Log In")
.performClick()
verify { viewModel.trySendAction(WelcomeAction.LoginClick) }
}
}
private val DEFAULT_STATE = WelcomeState(
index = 0,
pages = listOf(
WelcomeState.WelcomeCard.CardOne,
WelcomeState.WelcomeCard.CardTwo,
),
)

View file

@ -0,0 +1,95 @@
package com.x8bit.bitwarden.ui.auth.feature.welcome
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 WelcomeViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct`() = runTest {
val viewModel = WelcomeViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
}
}
@Test
fun `PagerSwipe should update state`() = runTest {
val viewModel = WelcomeViewModel()
val newIndex = 2
viewModel.trySendAction(WelcomeAction.PagerSwipe(index = newIndex))
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(index = newIndex),
awaitItem(),
)
}
}
@Test
fun `DotClick should update state and emit UpdatePager`() = runTest {
val viewModel = WelcomeViewModel()
val newIndex = 2
viewModel.trySendAction(WelcomeAction.DotClick(index = newIndex))
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(index = newIndex),
awaitItem(),
)
}
viewModel.eventFlow.test {
assertEquals(
WelcomeEvent.UpdatePager(index = newIndex),
awaitItem(),
)
}
}
@Test
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
val viewModel = WelcomeViewModel()
viewModel.trySendAction(WelcomeAction.CreateAccountClick)
viewModel.eventFlow.test {
assertEquals(
WelcomeEvent.NavigateToCreateAccount,
awaitItem(),
)
}
}
@Test
fun `LoginClick should emit NavigateToLogin`() = runTest {
val viewModel = WelcomeViewModel()
viewModel.trySendAction(WelcomeAction.LoginClick)
viewModel.eventFlow.test {
assertEquals(
WelcomeEvent.NavigateToLogin,
awaitItem(),
)
}
}
}
private val DEFAULT_STATE = WelcomeState(
index = 0,
pages = listOf(
WelcomeState.WelcomeCard.CardOne,
WelcomeState.WelcomeCard.CardTwo,
WelcomeState.WelcomeCard.CardThree,
WelcomeState.WelcomeCard.CardFour,
),
)

View file

@ -73,6 +73,15 @@ class RootNavScreenTest : BaseComposeTest() {
} }
assertTrue(isSplashScreenRemoved) assertTrue(isSplashScreenRemoved)
// Make sure navigating to Auth with the welcome route works as expected:
rootNavStateFlow.value = RootNavState.AuthWithWelcome
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "welcome",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault locked works as expected: // Make sure navigating to vault locked works as expected:
rootNavStateFlow.value = RootNavState.VaultLocked rootNavStateFlow.value = RootNavState.VaultLocked
composeTestRule.runOnIdle { composeTestRule.runOnIdle {

View file

@ -22,6 +22,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null) private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository = mockk<AuthRepository> { private val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { showWelcomeCarousel } returns false
} }
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
@ -29,7 +30,22 @@ class RootNavViewModelTest : BaseViewModelTest() {
fun `when there are no accounts the nav state should be Auth`() { fun `when there are no accounts the nav state should be Auth`() {
mutableUserStateFlow.tryEmit(null) mutableUserStateFlow.tryEmit(null)
val viewModel = createViewModel() val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value) assertEquals(
RootNavState.Auth,
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `when there are no accounts and the user has not logged on before the nav state should be Auth with the welcome route`() {
every { authRepository.showWelcomeCarousel } returns true
mutableUserStateFlow.tryEmit(null)
val viewModel = createViewModel()
assertEquals(
RootNavState.AuthWithWelcome,
viewModel.stateFlow.value,
)
} }
@Test @Test
@ -57,7 +73,10 @@ class RootNavViewModelTest : BaseViewModelTest() {
), ),
) )
val viewModel = createViewModel() val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value) assertEquals(
RootNavState.Auth,
viewModel.stateFlow.value,
)
} }
@Test @Test
@ -220,7 +239,10 @@ class RootNavViewModelTest : BaseViewModelTest() {
), ),
) )
val viewModel = createViewModel() val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value) assertEquals(
RootNavState.Auth,
viewModel.stateFlow.value,
)
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -250,7 +272,10 @@ class RootNavViewModelTest : BaseViewModelTest() {
), ),
) )
val viewModel = createViewModel() val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value) assertEquals(
RootNavState.Auth,
viewModel.stateFlow.value,
)
} }
@Test @Test