PM-19645: Remove the new device UI email access flow (#4996)

This commit is contained in:
David Perez
2025-04-04 14:41:34 -05:00
committed by GitHub
parent dda8237ce5
commit 4d9a19f43c
28 changed files with 1 additions and 2543 deletions

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@@ -344,16 +343,6 @@ interface AuthDiskSource {
*/
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
/**
* Gets the new device notice state for the given [userId].
*/
fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState
/**
* Stores the new device notice state for the given [userId].
*/
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
/**
* Gets the last lock timestamp for the given [userId].
*/

View File

@@ -5,8 +5,6 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@@ -49,7 +47,6 @@ private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
/**
@@ -489,22 +486,6 @@ class AuthDiskSourceImpl(
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
override fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState {
return getString(key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId))?.let {
json.decodeFromStringOrNull(it)
} ?: NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
}
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
putString(
key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId),
value = newState?.let { json.encodeToString(it) },
)
}
override fun getLastLockTimestamp(userId: String): Instant? {
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
Instant.ofEpochMilli(it)

View File

@@ -1,60 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.disk.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Describes the current display status of the new device notice screen.
*/
@Serializable
enum class NewDeviceNoticeDisplayStatus {
/**
* The user has seen the screen and indicated they can access their email.
*/
@SerialName("canAccessEmail")
CAN_ACCESS_EMAIL,
/**
* The user has indicated they can access their email
* as specified by the Permanent mode of the notice.
*/
@SerialName("canAccessEmailPermanent")
CAN_ACCESS_EMAIL_PERMANENT,
/**
* The user has not seen the screen.
*/
@SerialName("hasNotSeen")
HAS_NOT_SEEN,
/**
* The user has seen the screen and selected "remind me later".
*/
@SerialName("hasSeen")
HAS_SEEN,
}
/**
* The state of the new device notice screen.
*/
@Suppress("MagicNumber")
@Serializable
data class NewDeviceNoticeState(
@SerialName("displayStatus")
val displayStatus: NewDeviceNoticeDisplayStatus,
@SerialName("lastSeenDate")
@Contextual
val lastSeenDate: ZonedDateTime?,
) {
/**
* Whether the [lastSeenDate] is at least 7 days old.
*/
val shouldDisplayNoticeIfSeen = lastSeenDate
?.isBefore(
ZonedDateTime.now().minusDays(7),
)
?: false
}

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
@@ -423,19 +422,4 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(status: OnboardingStatus)
/**
* Checks if a new device notice should be displayed.
*/
fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean
/**
* Gets the new device notice state of active user.
*/
fun getNewDeviceNoticeState(): NewDeviceNoticeState?
/**
* Stores the new device notice state for active user.
*/
fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?)
}

View File

@@ -19,8 +19,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
@@ -117,7 +115,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
@@ -149,7 +146,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import java.time.ZonedDateTime
import javax.inject.Singleton
/**
@@ -1408,86 +1404,6 @@ class AuthRepositoryImpl(
}
}
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
return activeUserId?.let { userId ->
authDiskSource.getNewDeviceNoticeState(userId = userId)
}
}
override fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?) {
activeUserId?.let { userId ->
authDiskSource.storeNewDeviceNoticeState(userId = userId, newState = newState)
}
}
override fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean {
return activeUserId
?.let { userId ->
if (!newDeviceNoticePreConditionsValid()) {
return false
}
val newDeviceNoticeState = authDiskSource.getNewDeviceNoticeState(userId = userId)
return when (newDeviceNoticeState.displayStatus) {
// if the user has already attested email access but permanent flag is enabled,
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL -> true
// if the user has already seen but 7 days have already passed,
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.HAS_SEEN -> {
newDeviceNoticeState.shouldDisplayNoticeIfSeen
}
NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN -> true
// the user never needs to see the notice again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT -> false
}
}
?: false
}
/**
* Checks if the preconditions are met for a user to see a new device notice:
* - Must be a Bitwarden cloud user.
* - The account must be at least one week old.
* - Cannot have an active policy requiring SSO to be enabled.
* - Cannot have two-factor authentication enabled.
*/
private fun newDeviceNoticePreConditionsValid(): Boolean {
val checkEnvironment = !featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
val isSelfHosted = environmentRepository.environment.type == Environment.Type.SELF_HOSTED
if (checkEnvironment && isSelfHosted) {
return false
}
val userProfile = authDiskSource.userState?.activeAccount?.profile
val isProfileAtLeastWeekOld = userProfile
?.let {
it.creationDate
?.plusWeeks(1)
?.isBefore(
ZonedDateTime.now(),
)
}
?: false
if (!isProfileAtLeastWeekOld) {
return false
}
val hasTwoFactorEnabled = userProfile
?.isTwoFactorEnabled
?: false
if (hasTwoFactorEnabled) {
return false
}
val hasSSOPolicy =
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
.any { p -> p.isEnabled }
return !hasSSOPolicy
}
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,

View File

@@ -1,58 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val EMAIL_ADDRESS = "email_address"
private const val NEW_DEVICE_NOTICE_PREFIX = "new_device_notice"
private const val NEW_DEVICE_NOTICE_EMAIL_ACCESS_ROUTE =
"$NEW_DEVICE_NOTICE_PREFIX/{${EMAIL_ADDRESS}}"
/**
* Class to retrieve new device notice email access arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class NewDeviceNoticeEmailAccessArgs(val emailAddress: String) {
constructor(savedStateHandle: SavedStateHandle) : this(
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
)
}
/**
* Navigate to the new device notice email access screen.
*/
fun NavController.navigateToNewDeviceNoticeEmailAccess(
emailAddress: String,
navOptions: NavOptions? = null,
) {
this.navigate(
route = "$NEW_DEVICE_NOTICE_PREFIX/$emailAddress",
navOptions = navOptions,
)
}
/**
* Add the new device notice email access screen to the nav graph.
*/
fun NavGraphBuilder.newDeviceNoticeEmailAccessDestination(
onNavigateBackToVault: () -> Unit,
onNavigateToTwoFactorOptions: () -> Unit,
) {
composableWithSlideTransitions(
route = NEW_DEVICE_NOTICE_EMAIL_ACCESS_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
),
) {
NewDeviceNoticeEmailAccessScreen(
onNavigateBackToVault = onNavigateBackToVault,
onNavigateToTwoFactorOptions = onNavigateToTwoFactorOptions,
)
}
}

View File

@@ -1,215 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.ContinueClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.EmailAccessToggle
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.LearnMoreClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The top level composable for the new device notice email access screen.
*/
@Composable
fun NewDeviceNoticeEmailAccessScreen(
onNavigateBackToVault: () -> Unit,
onNavigateToTwoFactorOptions: () -> Unit,
viewModel: NewDeviceNoticeEmailAccessViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
NavigateToTwoFactorOptions -> onNavigateToTwoFactorOptions()
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault -> onNavigateBackToVault()
is NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore -> {
intentManager.launchUri(
"https://bitwarden.com/help/new-device-verification/"
.toUri(),
)
}
}
}
BitwardenScaffold {
NewDeviceNoticeEmailAccessContent(
email = state.email,
isEmailAccessEnabled = state.isEmailAccessEnabled,
onEmailAccessToggleChanged = remember(viewModel) {
{ newState ->
viewModel.trySendAction(EmailAccessToggle(isEnabled = newState))
}
},
onContinueClick = remember(viewModel) {
{ viewModel.trySendAction(ContinueClick) }
},
onLearnMoreClick = remember(viewModel) {
{ viewModel.trySendAction(LearnMoreClick) }
},
)
}
}
@Composable
private fun NewDeviceNoticeEmailAccessContent(
email: String,
isEmailAccessEnabled: Boolean,
onEmailAccessToggleChanged: (Boolean) -> Unit,
onContinueClick: () -> Unit,
onLearnMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.standardHorizontalMargin()
.fillMaxSize()
.verticalScroll(state = rememberScrollState()),
) {
Spacer(modifier = Modifier.height(104.dp))
HeaderContent(onLearnMoreClick = onLearnMoreClick)
Spacer(modifier = Modifier.height(24.dp))
MainContent(
email = email,
isEmailAccessEnabled = isEmailAccessEnabled,
onEmailAccessToggleChanged = onEmailAccessToggleChanged,
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenFilledButton(
label = stringResource(R.string.continue_text),
onClick = onContinueClick,
modifier = Modifier.fillMaxSize(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
/**
* Header content containing the warning icon and title.
*/
@Suppress("MaxLineLength")
@Composable
private fun ColumnScope.HeaderContent(
onLearnMoreClick: () -> Unit,
) {
Image(
painter = rememberVectorPainter(id = R.drawable.warning),
contentDescription = null,
modifier = Modifier.size(120.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.important_notice),
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(
R.string.bitwarden_will_soon_send_a_code_to_your_account_email_to_verify_logins_from_new_devices_in_february,
),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
)
BitwardenClickableText(
label = stringResource(id = R.string.learn_more),
onClick = onLearnMoreClick,
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
modifier = Modifier.testTag("LearnMoreLabel"),
)
}
/**
* The main content of the screen.
*/
@Composable
private fun MainContent(
email: String,
isEmailAccessEnabled: Boolean,
onEmailAccessToggleChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
) {
Text(
text = R.string.do_you_have_reliable_access_to_your_email.toAnnotatedString(
args = arrayOf(email),
style = SpanStyle(
color = BitwardenTheme.colorScheme.text.primary,
fontSize = BitwardenTheme.typography.bodyLarge.fontSize,
fontWeight = FontWeight.Normal,
),
emphasisHighlightStyle = SpanStyle(
color = BitwardenTheme.colorScheme.text.primary,
fontSize = BitwardenTheme.typography.bodyLarge.fontSize,
fontWeight = FontWeight.Bold,
),
),
)
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenSwitch(
label = stringResource(id = R.string.yes_i_can_reliably_access_my_email),
isChecked = isEmailAccessEnabled,
onCheckedChange = onEmailAccessToggleChanged,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("EmailAccessToggle"),
)
}
}
@PreviewScreenSizes
@Composable
private fun NewDeviceNoticeEmailAccessScreen_preview() {
BitwardenTheme {
NewDeviceNoticeEmailAccessContent(
email = "test@bitwarden.com",
isEmailAccessEnabled = true,
onEmailAccessToggleChanged = {},
onContinueClick = {},
onLearnMoreClick = {},
)
}
}

View File

@@ -1,131 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.ContinueClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.EmailAccessToggle
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.LearnMoreClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Manages application state for the new device notice email access screen.
*/
@HiltViewModel
class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<
NewDeviceNoticeEmailAccessState,
NewDeviceNoticeEmailAccessEvent,
NewDeviceNoticeEmailAccessAction,
>(
initialState = savedStateHandle[KEY_STATE]
?: NewDeviceNoticeEmailAccessState(
email = NewDeviceNoticeEmailAccessArgs(savedStateHandle).emailAddress,
isEmailAccessEnabled = false,
),
) {
init {
viewModelScope.launch {
vaultRepository.syncForResult()
if (!authRepository.checkUserNeedsNewDeviceTwoFactorNotice()) {
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateBackToVault)
}
}
}
override fun handleAction(action: NewDeviceNoticeEmailAccessAction) {
when (action) {
ContinueClick -> handleContinueClick()
is EmailAccessToggle -> handleEmailAccessToggle(action)
LearnMoreClick -> handleLearnMoreClick()
}
}
private fun handleContinueClick() {
if (state.isEmailAccessEnabled) {
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = CAN_ACCESS_EMAIL_PERMANENT,
lastSeenDate = null,
),
)
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateBackToVault)
} else {
sendEvent(NavigateToTwoFactorOptions)
}
}
private fun handleEmailAccessToggle(action: EmailAccessToggle) {
mutableStateFlow.update {
it.copy(isEmailAccessEnabled = action.isEnabled)
}
}
private fun handleLearnMoreClick() {
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore)
}
}
/**
* Models state of the new device notice email access screen.
*/
@Parcelize
data class NewDeviceNoticeEmailAccessState(
val email: String,
val isEmailAccessEnabled: Boolean,
) : Parcelable
/**
* Models events for the new device notice email access screen.
*/
sealed class NewDeviceNoticeEmailAccessEvent {
/**
* Navigates to the Two Factor Options screen.
*/
data object NavigateToTwoFactorOptions : NewDeviceNoticeEmailAccessEvent()
/**
* Navigates back.
*/
data object NavigateBackToVault : NewDeviceNoticeEmailAccessEvent()
/**
* Navigates to learn more about New Device Login Protection
*/
data object NavigateToLearnMore : NewDeviceNoticeEmailAccessEvent()
}
/**
* Models actions for the new device notice email access screen.
*/
sealed class NewDeviceNoticeEmailAccessAction {
/**
* User tapped the continue button.
*/
data object ContinueClick : NewDeviceNoticeEmailAccessAction()
/**
* User tapped the email access toggle.
*/
data class EmailAccessToggle(val isEnabled: Boolean) : NewDeviceNoticeEmailAccessAction()
/**
* User tapped the learn more button.
*/
data object LearnMoreClick : NewDeviceNoticeEmailAccessAction()
}

View File

@@ -1,37 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val NEW_DEVICE_NOTICE_TWO_FACTOR_ROUTE = "new_device_notice_two_factor"
/**
* Navigate to the new device notice two factor screen.
*/
fun NavController.navigateToNewDeviceNoticeTwoFactor(
navOptions: NavOptions? = null,
) {
this.navigate(
route = NEW_DEVICE_NOTICE_TWO_FACTOR_ROUTE,
navOptions = navOptions,
)
}
/**
* Add the new device notice two factor screen to the nav graph.
*/
fun NavGraphBuilder.newDeviceNoticeTwoFactorDestination(
onNavigateBackToVault: () -> Unit,
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = NEW_DEVICE_NOTICE_TWO_FACTOR_ROUTE,
) {
NewDeviceNoticeTwoFactorScreen(
onNavigateBackToVault = onNavigateBackToVault,
onNavigateBack = onNavigateBack,
)
}
}

View File

@@ -1,220 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.ContinueDialogClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.DismissDialogClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.NavigateBackClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorEvent.NavigateBackToVault
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The top level composable for the new device notice two factor screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun NewDeviceNoticeTwoFactorScreen(
onNavigateBackToVault: () -> Unit,
onNavigateBack: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: NewDeviceNoticeTwoFactorViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is NavigateToTurnOnTwoFactor -> {
intentManager.launchUri(event.url.toUri())
}
is NavigateToChangeAccountEmail -> {
intentManager.launchUri(event.url.toUri())
}
NavigateBackToVault -> onNavigateBackToVault()
NewDeviceNoticeTwoFactorEvent.NavigateBack -> onNavigateBack()
}
}
// Show dialog if needed:
when (val dialogState = state.dialogState) {
is NewDeviceNoticeTwoFactorDialogState.TurnOnTwoFactorDialog,
is NewDeviceNoticeTwoFactorDialogState.ChangeAccountEmailDialog,
->
BitwardenTwoButtonDialog(
title = stringResource(R.string.continue_to_web_app),
message = dialogState.message(),
confirmButtonText = stringResource(id = R.string.confirm),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = { viewModel.trySendAction(ContinueDialogClick) },
onDismissClick = { viewModel.trySendAction(DismissDialogClick) },
onDismissRequest = { viewModel.trySendAction(DismissDialogClick) },
)
null -> Unit
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
BitwardenScaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = "",
scrollBehavior = scrollBehavior,
navigationIcon = NavigationIcon(
navigationIcon = rememberVectorPainter(R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(NavigateBackClick)
}
},
),
)
},
) {
NewDeviceNoticeTwoFactorContent(
onTurnOnTwoFactorClick = {
viewModel.trySendAction(TurnOnTwoFactorClick)
},
onChangeAccountEmailClick = {
viewModel.trySendAction(ChangeAccountEmailClick)
},
)
}
}
/**
* The content of the screen.
*/
@Composable
private fun NewDeviceNoticeTwoFactorContent(
onTurnOnTwoFactorClick: () -> Unit,
onChangeAccountEmailClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.standardHorizontalMargin()
.fillMaxSize()
.verticalScroll(state = rememberScrollState()),
) {
Spacer(modifier = Modifier.height(104.dp))
HeaderContent()
Spacer(modifier = Modifier.height(24.dp))
MainContent(
onTurnOnTwoFactorClick = onTurnOnTwoFactorClick,
onChangeAccountEmailClick = onChangeAccountEmailClick,
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
/**
* Header content containing the user lock icon and title.
*/
@Suppress("MaxLineLength")
@Composable
private fun ColumnScope.HeaderContent() {
Image(
painter = rememberVectorPainter(id = R.drawable.user_lock),
contentDescription = null,
modifier = Modifier.size(120.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.set_up_two_step_login),
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(
R.string.you_can_set_up_two_step_login_as_an_alternative_way_to_protect_your_account_or_change_your_email_to_one_you_can_access,
),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
)
}
/**
* The content containing the external links and remind me buttons.
*/
@Composable
private fun ColumnScope.MainContent(
onTurnOnTwoFactorClick: () -> Unit,
onChangeAccountEmailClick: () -> Unit,
) {
BitwardenFilledButton(
label = stringResource(R.string.turn_on_two_step_login),
onClick = onTurnOnTwoFactorClick,
icon = rememberVectorPainter(id = R.drawable.ic_external_link),
modifier = Modifier
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
BitwardenOutlinedButton(
label = stringResource(R.string.change_account_email),
onClick = onChangeAccountEmailClick,
icon = rememberVectorPainter(id = R.drawable.ic_external_link),
modifier = Modifier
.fillMaxWidth(),
)
}
@PreviewScreenSizes
@Composable
private fun NewDeviceNoticeTwoFactorScreen_preview() {
BitwardenTheme {
NewDeviceNoticeTwoFactorContent(
onTurnOnTwoFactorClick = {},
onChangeAccountEmailClick = {},
)
}
}

View File

@@ -1,203 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.ContinueDialogClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.DismissDialogClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.NavigateBackClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorDialogState.ChangeAccountEmailDialog
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorDialogState.TurnOnTwoFactorDialog
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/**
* Manages application state for the new device notice two factor screen.
*/
@HiltViewModel
class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
val authRepository: AuthRepository,
val environmentRepository: EnvironmentRepository,
val settingsRepository: SettingsRepository,
val vaultRepository: VaultRepository,
) : BaseViewModel<
NewDeviceNoticeTwoFactorState,
NewDeviceNoticeTwoFactorEvent,
NewDeviceNoticeTwoFactorAction,
>(
initialState = NewDeviceNoticeTwoFactorState(
dialogState = null,
),
) {
init {
viewModelScope.launch {
vaultRepository.syncForResult()
if (!authRepository.checkUserNeedsNewDeviceTwoFactorNotice()) {
sendEvent(NewDeviceNoticeTwoFactorEvent.NavigateBackToVault)
}
}
}
private val webTwoFactorUrl: String
get() {
val baseUrl = environmentRepository
.environment
.environmentUrlData
.baseWebVaultUrlOrDefault
return "$baseUrl/#/settings/security/two-factor"
}
private val webAccountUrl: String
get() {
val baseUrl = environmentRepository
.environment
.environmentUrlData
.baseWebVaultUrlOrDefault
return "$baseUrl/#/settings/account"
}
override fun handleAction(action: NewDeviceNoticeTwoFactorAction) {
when (action) {
ChangeAccountEmailClick -> updateDialogState(newState = ChangeAccountEmailDialog)
TurnOnTwoFactorClick -> updateDialogState(newState = TurnOnTwoFactorDialog)
DismissDialogClick -> updateDialogState(newState = null)
ContinueDialogClick -> handleContinueDialog()
NavigateBackClick -> sendEvent(NewDeviceNoticeTwoFactorEvent.NavigateBack)
}
}
private fun handleContinueDialog() {
when (state.dialogState) {
is ChangeAccountEmailDialog -> {
// when the user leaves the app set sync date to null to force a sync on next unlock
settingsRepository.vaultLastSync = null
sendEvent(
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(url = webAccountUrl),
)
updateDialogState(newState = null)
}
is TurnOnTwoFactorDialog -> {
// when the user leaves the app set sync date to null to force a sync on next unlock
settingsRepository.vaultLastSync = null
sendEvent(
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(url = webTwoFactorUrl),
)
updateDialogState(newState = null)
}
null -> return
}
}
private fun updateDialogState(newState: NewDeviceNoticeTwoFactorDialogState?) {
mutableStateFlow.update {
it.copy(dialogState = newState)
}
}
}
/**
* Models events for the new device notice two factor screen.
*/
sealed class NewDeviceNoticeTwoFactorEvent {
/**
* Navigates to the turn on two factor url.
* @param url The url to navigate to.
*/
data class NavigateToTurnOnTwoFactor(val url: String) : NewDeviceNoticeTwoFactorEvent()
/**
* Navigates to the change account email url.
* @param url The url to navigate to.
*/
data class NavigateToChangeAccountEmail(val url: String) : NewDeviceNoticeTwoFactorEvent()
/**
* Navigates back to vault.
*/
data object NavigateBackToVault : NewDeviceNoticeTwoFactorEvent()
/**
* Navigates back to previous screen.
*/
data object NavigateBack : NewDeviceNoticeTwoFactorEvent()
}
/**
* Models actions for the new device notice two factor screen.
*/
sealed class NewDeviceNoticeTwoFactorAction {
/**
* User tapped the turn on two factor button.
*/
data object TurnOnTwoFactorClick : NewDeviceNoticeTwoFactorAction()
/**
* User tapped the change account email button.
*/
data object ChangeAccountEmailClick : NewDeviceNoticeTwoFactorAction()
/**
* User tapped the dismiss dialog button.
*/
data object DismissDialogClick : NewDeviceNoticeTwoFactorAction()
/**
* User tapped the continue dialog button.
*/
data object ContinueDialogClick : NewDeviceNoticeTwoFactorAction()
/**
* User tapped the back button.
*/
data object NavigateBackClick : NewDeviceNoticeTwoFactorAction()
}
/**
* Models state of the new device notice two factor screen.
*/
@Parcelize
data class NewDeviceNoticeTwoFactorState(
val dialogState: NewDeviceNoticeTwoFactorDialogState?,
) : Parcelable
/**
* Dialog states for the new device notice two factor screen.
*/
sealed class NewDeviceNoticeTwoFactorDialogState(
val message: Text,
) : Parcelable {
/**
* Represents the turn on two factor dialog.
*/
@Parcelize
data object TurnOnTwoFactorDialog : NewDeviceNoticeTwoFactorDialogState(
message = R.string.two_step_login_description_long.asText(),
)
/**
* Represents the change account email dialog.
*/
@Parcelize
data object ChangeAccountEmailDialog : NewDeviceNoticeTwoFactorDialogState(
R.string.you_can_change_your_account_email_on_the_bitwarden_web_app.asText(),
)
}

View File

@@ -28,7 +28,6 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.navigateToExpiredRegistrationLinkScreen
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.navigateToNewDeviceNoticeEmailAccess
import com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout.navigateToPreventAccountLockout
import com.x8bit.bitwarden.ui.auth.feature.removepassword.REMOVE_PASSWORD_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword
@@ -130,7 +129,6 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForFido2Save,
is RootNavState.VaultUnlockedForFido2Assertion,
is RootNavState.VaultUnlockedForFido2GetCredentials,
is RootNavState.NewDeviceTwoFactorNotice,
-> VAULT_UNLOCKED_GRAPH_ROUTE
RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_AS_ROOT_ROUTE
@@ -263,13 +261,6 @@ fun RootNavScreen(
RootNavState.OnboardingStepsComplete -> {
navController.navigateToSetupCompleteScreen(rootNavOptions)
}
is RootNavState.NewDeviceTwoFactorNotice -> {
navController.navigateToNewDeviceNoticeEmailAccess(
emailAddress = currentState.email,
navOptions = rootNavOptions,
)
}
}
}
}

View File

@@ -103,11 +103,6 @@ class RootNavViewModel @Inject constructor(
}
}
userState.activeAccount.isVaultUnlocked &&
authRepository.checkUserNeedsNewDeviceTwoFactorNotice() -> RootNavState.NewDeviceTwoFactorNotice(
userState.activeAccount.email,
)
userState.activeAccount.isVaultUnlocked -> {
when (specialCircumstance) {
is SpecialCircumstance.AutofillSave -> {
@@ -375,14 +370,6 @@ sealed class RootNavState : Parcelable {
*/
@Parcelize
data object OnboardingStepsComplete : RootNavState()
/**
* App should show the new device two factor notice screen.
*/
@Parcelize
data class NewDeviceTwoFactorNotice(
val email: String,
) : RootNavState()
}
/**

View File

@@ -8,9 +8,6 @@ import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillS
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreen
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestination
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestination
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.navigateToNewDeviceNoticeTwoFactor
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.newDeviceNoticeEmailAccessDestination
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.newDeviceNoticeTwoFactorDestination
import com.x8bit.bitwarden.ui.platform.feature.search.navigateToSearch
import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
@@ -228,13 +225,5 @@ fun NavGraphBuilder.vaultUnlockedGraph(
importLoginsScreenDestination(
onNavigateBack = { navController.popBackStack() },
)
newDeviceNoticeEmailAccessDestination(
onNavigateBackToVault = { navController.navigateToVaultUnlockedGraph() },
onNavigateToTwoFactorOptions = { navController.navigateToNewDeviceNoticeTwoFactor() },
)
newDeviceNoticeTwoFactorDestination(
onNavigateBackToVault = { navController.navigateToVaultUnlockedGraph() },
onNavigateBack = { navController.popBackStack() },
)
}
}

View File

@@ -1,58 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="96"
android:viewportHeight="96">
<path
android:pathData="M0,18C0,14.686 2.686,12 6,12H60C63.314,12 66,14.686 66,18V56C66,59.314 63.314,62 60,62H6C2.686,62 0,59.314 0,56V18Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M60,14H6C3.791,14 2,15.791 2,18V56C2,58.209 3.791,60 6,60H60C62.209,60 64,58.209 64,56V18C64,15.791 62.209,14 60,14ZM6,12C2.686,12 0,14.686 0,18V56C0,59.314 2.686,62 6,62H60C63.314,62 66,59.314 66,56V18C66,14.686 63.314,12 60,12H6Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M33,58C41.837,58 49,50.837 49,42C49,33.163 41.837,26 33,26C24.163,26 17,33.163 17,42C17,50.837 24.163,58 33,58ZM26,36C24.343,36 23,37.343 23,39V49C23,50.657 24.343,52 26,52H40C41.657,52 43,50.657 43,49V39C43,37.343 41.657,36 40,36H26Z"
android:fillColor="#79A1E9"
android:fillType="evenOdd"/>
<path
android:pathData="M23,39C23,37.343 24.343,36 26,36H40C41.657,36 43,37.343 43,39V49C43,50.657 41.657,52 40,52H26C24.343,52 23,50.657 23,49V39Z"
android:fillColor="#FFBF00"/>
<path
android:pathData="M40,38H26C25.448,38 25,38.448 25,39V49C25,49.552 25.448,50 26,50H40C40.552,50 41,49.552 41,49V39C41,38.448 40.552,38 40,38ZM26,36C24.343,36 23,37.343 23,39V49C23,50.657 24.343,52 26,52H40C41.657,52 43,50.657 43,49V39C43,37.343 41.657,36 40,36H26Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M32,42C32,41.448 32.448,41 33,41C33.552,41 34,41.448 34,42V46C34,46.552 33.552,47 33,47C32.448,47 32,46.552 32,46V42Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M28,35C28,32.239 30.239,30 33,30C35.761,30 38,32.239 38,35V36H36V35C36,33.343 34.657,32 33,32C31.343,32 30,33.343 30,35V36H28V35Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M65,24H1V22H65V24Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M62,18C62,19.105 61.105,20 60,20C58.895,20 58,19.105 58,18C58,16.895 58.895,16 60,16C61.105,16 62,16.895 62,18Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M56,18C56,19.105 55.105,20 54,20C52.895,20 52,19.105 52,18C52,16.895 52.895,16 54,16C55.105,16 56,16.895 56,18Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M50,18C50,19.105 49.105,20 48,20C46.895,20 46,19.105 46,18C46,16.895 46.895,16 48,16C49.105,16 50,16.895 50,18Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M82,42C82,49.732 75.732,56 68,56C60.268,56 54,49.732 54,42C54,34.268 60.268,28 68,28C75.732,28 82,34.268 82,42Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M68,54C74.627,54 80,48.627 80,42C80,35.373 74.627,30 68,30C61.373,30 56,35.373 56,42C56,48.627 61.373,54 68,54ZM68,56C75.732,56 82,49.732 82,42C82,34.268 75.732,28 68,28C60.268,28 54,34.268 54,42C54,49.732 60.268,56 68,56Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M93.907,71.976C95.289,75.313 96,78.889 96,82.5C96,83.328 95.328,84 94.5,84H42.5C41.672,84 41,83.328 41,82.5C41,78.889 41.711,75.313 43.093,71.976C44.475,68.64 46.501,65.608 49.055,63.055C51.608,60.501 54.64,58.475 57.976,57.093C61.313,55.711 64.889,55 68.5,55C72.111,55 75.687,55.711 79.024,57.093C82.36,58.475 85.392,60.501 87.945,63.055C90.499,65.608 92.525,68.64 93.907,71.976Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M93.995,82C93.933,78.822 93.277,75.681 92.059,72.742C90.777,69.648 88.899,66.837 86.531,64.469C84.163,62.101 81.352,60.223 78.258,58.941C75.165,57.66 71.849,57 68.5,57C65.151,57 61.835,57.66 58.742,58.941C55.648,60.223 52.837,62.101 50.469,64.469C48.101,66.837 46.223,69.648 44.941,72.742C43.723,75.681 43.067,78.822 43.005,82H93.995ZM96,82.5C96,78.889 95.289,75.313 93.907,71.976C92.525,68.64 90.499,65.608 87.945,63.055C85.392,60.501 82.36,58.475 79.024,57.093C75.687,55.711 72.111,55 68.5,55C64.889,55 61.313,55.711 57.976,57.093C54.64,58.475 51.608,60.501 49.055,63.055C46.501,65.608 44.475,68.64 43.093,71.976C41.711,75.313 41,78.889 41,82.5C41,83.328 41.672,84 42.5,84H94.5C95.328,84 96,83.328 96,82.5Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
</vector>

View File

@@ -1,93 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="96"
android:viewportHeight="96">
<path
android:pathData="M54,8a8,8 0,0 1,8 -8h8a8,8 0,0 1,8 8v4H54V8Z"
android:fillColor="#79A1E9"/>
<path
android:pathData="M70,2h-8a6,6 0,0 0,-6 6v2h20L76,8a6,6 0,0 0,-6 -6ZM62,0a8,8 0,0 0,-8 8v4h24L78,8a8,8 0,0 0,-8 -8h-8Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M47,16a8,8 0,0 1,8 -8h22a8,8 0,0 1,8 8v6H47v-6Z"
android:fillColor="#79A1E9"/>
<path
android:pathData="M77,10H55a6,6 0,0 0,-6 6v4h34v-4a6,6 0,0 0,-6 -6ZM55,8a8,8 0,0 0,-8 8v6h38v-6a8,8 0,0 0,-8 -8H55Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M40,26a8,8 0,0 1,8 -8h36a8,8 0,0 1,8 8v66H40V26Z"
android:fillColor="#79A1E9"/>
<path
android:pathData="M84,20L48,20a6,6 0,0 0,-6 6v64h48L90,26a6,6 0,0 0,-6 -6ZM48,18a8,8 0,0 0,-8 8v66h52L92,26a8,8 0,0 0,-8 -8L48,18Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M4,44a8,8 0,0 1,8 -8h38a8,8 0,0 1,8 8v48H4V44Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M50,38L12,38a6,6 0,0 0,-6 6v46h50L56,44a6,6 0,0 0,-6 -6ZM12,36a8,8 0,0 0,-8 8v48h54L58,44a8,8 0,0 0,-8 -8L12,36Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M68.677,60.435c1.928,-3.316 6.718,-3.316 8.645,0l16.31,28.052C95.57,91.82 93.165,96 89.309,96H56.691c-3.856,0 -6.26,-4.18 -4.323,-7.513l16.31,-28.052Z"
android:fillColor="#FFBF00"/>
<path
android:pathData="M91.903,89.492 L75.593,61.44c-1.156,-1.99 -4.03,-1.99 -5.187,0L54.097,89.492c-1.163,2 0.28,4.508 2.594,4.508h32.618c2.314,0 3.757,-2.508 2.594,-4.508ZM77.323,60.435c-1.928,-3.316 -6.718,-3.316 -8.645,0l-16.31,28.052C50.43,91.82 52.835,96 56.691,96h32.618c3.856,0 6.26,-4.18 4.323,-7.513l-16.31,-28.052Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M75,88a2,2 0,1 1,-4 0,2 2,0 0,1 4,0ZM70.06,70.553a0.5,0.5 0,0 1,0.496 -0.553h4.888a0.5,0.5 0,0 1,0.497 0.553l-1.393,13a0.5,0.5 0,0 1,-0.497 0.447h-2.102a0.5,0.5 0,0 1,-0.497 -0.447l-1.393,-13Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M21,80a8,8 0,0 1,8 -8h4a8,8 0,0 1,8 8v12H21V80Z"
android:fillColor="#79A1E9"/>
<path
android:pathData="M33,74h-4a6,6 0,0 0,-6 6v10h16L39,80a6,6 0,0 0,-6 -6ZM29,72a8,8 0,0 0,-8 8v12h20L41,80a8,8 0,0 0,-8 -8h-4Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M13,46a1,1 0,0 1,1 -1h8a1,1 0,0 1,1 1v8a1,1 0,0 1,-1 1h-8a1,1 0,0 1,-1 -1v-8Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M15,47v6h6v-6h-6ZM14,45a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h8a1,1 0,0 0,1 -1v-8a1,1 0,0 0,-1 -1h-8Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M13,59a1,1 0,0 1,1 -1h8a1,1 0,0 1,1 1v8a1,1 0,0 1,-1 1h-8a1,1 0,0 1,-1 -1v-8Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M15,60v6h6v-6h-6ZM14,58a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h8a1,1 0,0 0,1 -1v-8a1,1 0,0 0,-1 -1h-8Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M26,46a1,1 0,0 1,1 -1h8a1,1 0,0 1,1 1v8a1,1 0,0 1,-1 1h-8a1,1 0,0 1,-1 -1v-8Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M28,47v6h6v-6h-6ZM27,45a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h8a1,1 0,0 0,1 -1v-8a1,1 0,0 0,-1 -1h-8Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M26,59a1,1 0,0 1,1 -1h8a1,1 0,0 1,1 1v8a1,1 0,0 1,-1 1h-8a1,1 0,0 1,-1 -1v-8Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M28,60v6h6v-6h-6ZM27,58a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h8a1,1 0,0 0,1 -1v-8a1,1 0,0 0,-1 -1h-8Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M39,59a1,1 0,0 1,1 -1h8a1,1 0,0 1,1 1v8a1,1 0,0 1,-1 1h-8a1,1 0,0 1,-1 -1v-8Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M41,60v6h6v-6h-6ZM40,58a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h8a1,1 0,0 0,1 -1v-8a1,1 0,0 0,-1 -1h-8Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M39,46a1,1 0,0 1,1 -1h8a1,1 0,0 1,1 1v8a1,1 0,0 1,-1 1h-8a1,1 0,0 1,-1 -1v-8Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M41,47v6h6v-6h-6ZM40,45a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h8a1,1 0,0 0,1 -1v-8a1,1 0,0 0,-1 -1h-8ZM47,27a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM58,26a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM67,27a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM77,27a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM77,33a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM78,38a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM77,45a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM78,50a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM68,32a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM67,39a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM68,44a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM67,51a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM58,32a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM47,33a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM58,38a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6ZM57,45a1,1 0,0 1,1 -1h6a1,1 0,1 1,0 2h-6a1,1 0,0 1,-1 -1ZM58,50a1,1 0,1 0,0 2h6a1,1 0,1 0,0 -2h-6Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
</vector>

View File

@@ -1,58 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="96"
android:viewportHeight="96">
<path
android:pathData="M0,18C0,14.686 2.686,12 6,12H60C63.314,12 66,14.686 66,18V56C66,59.314 63.314,62 60,62H6C2.686,62 0,59.314 0,56V18Z"
android:fillColor="#DBE5F6"/>
<path
android:pathData="M60,14H6C3.791,14 2,15.791 2,18V56C2,58.209 3.791,60 6,60H60C62.209,60 64,58.209 64,56V18C64,15.791 62.209,14 60,14ZM6,12C2.686,12 0,14.686 0,18V56C0,59.314 2.686,62 6,62H60C63.314,62 66,59.314 66,56V18C66,14.686 63.314,12 60,12H6Z"
android:fillColor="#0E3781"
android:fillType="evenOdd"/>
<path
android:pathData="M33,58C41.837,58 49,50.837 49,42C49,33.163 41.837,26 33,26C24.163,26 17,33.163 17,42C17,50.837 24.163,58 33,58ZM26,36C24.343,36 23,37.343 23,39V49C23,50.657 24.343,52 26,52H40C41.657,52 43,50.657 43,49V39C43,37.343 41.657,36 40,36H26Z"
android:fillColor="#99BAF4"
android:fillType="evenOdd"/>
<path
android:pathData="M23,39C23,37.343 24.343,36 26,36H40C41.657,36 43,37.343 43,39V49C43,50.657 41.657,52 40,52H26C24.343,52 23,50.657 23,49V39Z"
android:fillColor="#FFBF00"/>
<path
android:pathData="M40,38H26C25.448,38 25,38.448 25,39V49C25,49.552 25.448,50 26,50H40C40.552,50 41,49.552 41,49V39C41,38.448 40.552,38 40,38ZM26,36C24.343,36 23,37.343 23,39V49C23,50.657 24.343,52 26,52H40C41.657,52 43,50.657 43,49V39C43,37.343 41.657,36 40,36H26Z"
android:fillColor="#0E3781"
android:fillType="evenOdd"/>
<path
android:pathData="M32,42C32,41.448 32.448,41 33,41C33.552,41 34,41.448 34,42V46C34,46.552 33.552,47 33,47C32.448,47 32,46.552 32,46V42Z"
android:fillColor="#0E3781"/>
<path
android:pathData="M28,35C28,32.239 30.239,30 33,30C35.761,30 38,32.239 38,35V36H36V35C36,33.343 34.657,32 33,32C31.343,32 30,33.343 30,35V36H28V35Z"
android:fillColor="#0E3781"
android:fillType="evenOdd"/>
<path
android:pathData="M65,24H1V22H65V24Z"
android:fillColor="#0E3781"
android:fillType="evenOdd"/>
<path
android:pathData="M62,18C62,19.105 61.105,20 60,20C58.895,20 58,19.105 58,18C58,16.895 58.895,16 60,16C61.105,16 62,16.895 62,18Z"
android:fillColor="#0E3781"/>
<path
android:pathData="M56,18C56,19.105 55.105,20 54,20C52.895,20 52,19.105 52,18C52,16.895 52.895,16 54,16C55.105,16 56,16.895 56,18Z"
android:fillColor="#0E3781"/>
<path
android:pathData="M50,18C50,19.105 49.105,20 48,20C46.895,20 46,19.105 46,18C46,16.895 46.895,16 48,16C49.105,16 50,16.895 50,18Z"
android:fillColor="#0E3781"/>
<path
android:pathData="M82,42C82,49.732 75.732,56 68,56C60.268,56 54,49.732 54,42C54,34.268 60.268,28 68,28C75.732,28 82,34.268 82,42Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M68,54C74.627,54 80,48.627 80,42C80,35.373 74.627,30 68,30C61.373,30 56,35.373 56,42C56,48.627 61.373,54 68,54ZM68,56C75.732,56 82,49.732 82,42C82,34.268 75.732,28 68,28C60.268,28 54,34.268 54,42C54,49.732 60.268,56 68,56Z"
android:fillColor="#0E3781"
android:fillType="evenOdd"/>
<path
android:pathData="M93.907,71.976C95.289,75.313 96,78.889 96,82.5C96,83.328 95.328,84 94.5,84H42.5C41.672,84 41,83.328 41,82.5C41,78.889 41.711,75.313 43.093,71.976C44.475,68.64 46.501,65.608 49.055,63.055C51.608,60.501 54.64,58.475 57.976,57.093C61.313,55.711 64.889,55 68.5,55C72.111,55 75.687,55.711 79.024,57.093C82.36,58.475 85.392,60.501 87.945,63.055C90.499,65.608 92.525,68.64 93.907,71.976Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M93.995,82C93.933,78.822 93.277,75.681 92.059,72.742C90.777,69.648 88.899,66.837 86.531,64.469C84.163,62.101 81.352,60.223 78.258,58.941C75.165,57.66 71.849,57 68.5,57C65.151,57 61.835,57.66 58.742,58.941C55.648,60.223 52.837,62.101 50.469,64.469C48.101,66.837 46.223,69.648 44.941,72.742C43.723,75.681 43.067,78.822 43.005,82H93.995ZM96,82.5C96,78.889 95.289,75.313 93.907,71.976C92.525,68.64 90.499,65.608 87.945,63.055C85.392,60.501 82.36,58.475 79.024,57.093C75.687,55.711 72.111,55 68.5,55C64.889,55 61.313,55.711 57.976,57.093C54.64,58.475 51.608,60.501 49.055,63.055C46.501,65.608 44.475,68.64 43.093,71.976C41.711,75.313 41,78.889 41,82.5C41,83.328 41.672,84 42.5,84H94.5C95.328,84 96,83.328 96,82.5Z"
android:fillColor="#0E3781"
android:fillType="evenOdd"/>
</vector>

View File

@@ -1,104 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="124dp"
android:height="124dp"
android:viewportWidth="124"
android:viewportHeight="124">
<group>
<clip-path
android:pathData="M0,0h124v124h-124z"/>
<path
android:pathData="M69.75,10.33C69.75,4.63 74.38,0 80.08,0H90.42C96.12,0 100.75,4.63 100.75,10.33V15.5H69.75V10.33Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M90.42,2.58H80.08C75.8,2.58 72.33,6.05 72.33,10.33V12.92H98.17V10.33C98.17,6.05 94.7,2.58 90.42,2.58ZM80.08,0C74.38,0 69.75,4.63 69.75,10.33V15.5H100.75V10.33C100.75,4.63 96.12,0 90.42,0H80.08Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M60.71,20.67C60.71,14.96 65.33,10.33 71.04,10.33H99.46C105.17,10.33 109.79,14.96 109.79,20.67V28.42H60.71V20.67Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M99.46,12.92H71.04C66.76,12.92 63.29,16.39 63.29,20.67V25.83H107.21V20.67C107.21,16.39 103.74,12.92 99.46,12.92ZM71.04,10.33C65.33,10.33 60.71,14.96 60.71,20.67V28.42H109.79V20.67C109.79,14.96 105.17,10.33 99.46,10.33H71.04Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M51.67,33.58C51.67,27.88 56.29,23.25 62,23.25H108.5C114.21,23.25 118.83,27.88 118.83,33.58V118.83H51.67V33.58Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M108.5,25.83H62C57.72,25.83 54.25,29.3 54.25,33.58V116.25H116.25V33.58C116.25,29.3 112.78,25.83 108.5,25.83ZM62,23.25C56.29,23.25 51.67,27.88 51.67,33.58V118.83H118.83V33.58C118.83,27.88 114.21,23.25 108.5,23.25H62Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M5.17,56.83C5.17,51.13 9.79,46.5 15.5,46.5H64.58C70.29,46.5 74.92,51.13 74.92,56.83V118.83H5.17V56.83Z"
android:fillColor="#DBE5F6"/>
<path
android:pathData="M64.58,49.08H15.5C11.22,49.08 7.75,52.55 7.75,56.83V116.25H72.33V56.83C72.33,52.55 68.86,49.08 64.58,49.08ZM15.5,46.5C9.79,46.5 5.17,51.13 5.17,56.83V118.83H74.92V56.83C74.92,51.13 70.29,46.5 64.58,46.5H15.5Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M88.71,78.06C91.2,73.78 97.38,73.78 99.87,78.06L120.94,114.29C123.44,118.6 120.34,124 115.36,124H73.23C68.25,124 65.14,118.6 67.64,114.29L88.71,78.06Z"
android:fillColor="#FFBF00"/>
<path
android:pathData="M118.71,115.59L97.64,79.36C96.15,76.79 92.44,76.79 90.94,79.36L69.88,115.59C68.37,118.18 70.24,121.42 73.23,121.42H115.36C118.35,121.42 120.21,118.18 118.71,115.59ZM99.87,78.06C97.38,73.78 91.2,73.78 88.71,78.06L67.64,114.29C65.14,118.6 68.25,124 73.23,124H115.36C120.34,124 123.44,118.6 120.94,114.29L99.87,78.06Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M96.88,113.67C96.88,115.09 95.72,116.25 94.29,116.25C92.86,116.25 91.71,115.09 91.71,113.67C91.71,112.24 92.86,111.08 94.29,111.08C95.72,111.08 96.88,112.24 96.88,113.67Z"
android:fillColor="#020F66"/>
<path
android:pathData="M90.49,91.13C90.45,90.75 90.75,90.42 91.14,90.42H97.45C97.83,90.42 98.13,90.75 98.09,91.13L96.29,107.92C96.26,108.25 95.98,108.5 95.65,108.5H92.93C92.6,108.5 92.33,108.25 92.29,107.92L90.49,91.13Z"
android:fillColor="#020F66"/>
<path
android:pathData="M27.13,103.33C27.13,97.63 31.75,93 37.46,93H42.63C48.33,93 52.96,97.63 52.96,103.33V118.83H27.13V103.33Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M42.63,95.58H37.46C33.18,95.58 29.71,99.05 29.71,103.33V116.25H50.38V103.33C50.38,99.05 46.91,95.58 42.63,95.58ZM37.46,93C31.75,93 27.13,97.63 27.13,103.33V118.83H52.96V103.33C52.96,97.63 48.33,93 42.63,93H37.46Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M16.79,59.42C16.79,58.7 17.37,58.13 18.08,58.13H28.42C29.13,58.13 29.71,58.7 29.71,59.42V69.75C29.71,70.46 29.13,71.04 28.42,71.04H18.08C17.37,71.04 16.79,70.46 16.79,69.75V59.42Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M19.38,60.71V68.46H27.13V60.71H19.38ZM18.08,58.13C17.37,58.13 16.79,58.7 16.79,59.42V69.75C16.79,70.46 17.37,71.04 18.08,71.04H28.42C29.13,71.04 29.71,70.46 29.71,69.75V59.42C29.71,58.7 29.13,58.13 28.42,58.13H18.08Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M16.79,76.21C16.79,75.49 17.37,74.92 18.08,74.92H28.42C29.13,74.92 29.71,75.49 29.71,76.21V86.54C29.71,87.25 29.13,87.83 28.42,87.83H18.08C17.37,87.83 16.79,87.25 16.79,86.54V76.21Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M19.38,77.5V85.25H27.13V77.5H19.38ZM18.08,74.92C17.37,74.92 16.79,75.49 16.79,76.21V86.54C16.79,87.25 17.37,87.83 18.08,87.83H28.42C29.13,87.83 29.71,87.25 29.71,86.54V76.21C29.71,75.49 29.13,74.92 28.42,74.92H18.08Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M33.58,59.42C33.58,58.7 34.16,58.13 34.88,58.13H45.21C45.92,58.13 46.5,58.7 46.5,59.42V69.75C46.5,70.46 45.92,71.04 45.21,71.04H34.88C34.16,71.04 33.58,70.46 33.58,69.75V59.42Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M36.17,60.71V68.46H43.92V60.71H36.17ZM34.88,58.13C34.16,58.13 33.58,58.7 33.58,59.42V69.75C33.58,70.46 34.16,71.04 34.88,71.04H45.21C45.92,71.04 46.5,70.46 46.5,69.75V59.42C46.5,58.7 45.92,58.13 45.21,58.13H34.88Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M33.58,76.21C33.58,75.49 34.16,74.92 34.88,74.92H45.21C45.92,74.92 46.5,75.49 46.5,76.21V86.54C46.5,87.25 45.92,87.83 45.21,87.83H34.88C34.16,87.83 33.58,87.25 33.58,86.54V76.21Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M36.17,77.5V85.25H43.92V77.5H36.17ZM34.88,74.92C34.16,74.92 33.58,75.49 33.58,76.21V86.54C33.58,87.25 34.16,87.83 34.88,87.83H45.21C45.92,87.83 46.5,87.25 46.5,86.54V76.21C46.5,75.49 45.92,74.92 45.21,74.92H34.88Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M50.38,76.21C50.38,75.49 50.95,74.92 51.67,74.92H62C62.71,74.92 63.29,75.49 63.29,76.21V86.54C63.29,87.25 62.71,87.83 62,87.83H51.67C50.95,87.83 50.38,87.25 50.38,86.54V76.21Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M52.96,77.5V85.25H60.71V77.5H52.96ZM51.67,74.92C50.95,74.92 50.38,75.49 50.38,76.21V86.54C50.38,87.25 50.95,87.83 51.67,87.83H62C62.71,87.83 63.29,87.25 63.29,86.54V76.21C63.29,75.49 62.71,74.92 62,74.92H51.67Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M50.38,59.42C50.38,58.7 50.95,58.13 51.67,58.13H62C62.71,58.13 63.29,58.7 63.29,59.42V69.75C63.29,70.46 62.71,71.04 62,71.04H51.67C50.95,71.04 50.38,70.46 50.38,69.75V59.42Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M52.96,60.71V68.46H60.71V60.71H52.96ZM51.67,58.13C50.95,58.13 50.38,58.7 50.38,59.42V69.75C50.38,70.46 50.95,71.04 51.67,71.04H62C62.71,71.04 63.29,70.46 63.29,69.75V59.42C63.29,58.7 62.71,58.13 62,58.13H51.67Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M60.71,34.88C60.71,34.16 61.29,33.58 62,33.58H69.75C70.46,33.58 71.04,34.16 71.04,34.88C71.04,35.59 70.46,36.17 69.75,36.17L62,36.17C61.29,36.17 60.71,35.59 60.71,34.88ZM74.92,33.58C74.2,33.58 73.63,34.16 73.63,34.88C73.63,35.59 74.2,36.17 74.92,36.17L82.67,36.17C83.38,36.17 83.96,35.59 83.96,34.88C83.96,34.16 83.38,33.58 82.67,33.58H74.92ZM86.54,34.88C86.54,34.16 87.12,33.58 87.83,33.58H95.58C96.3,33.58 96.88,34.16 96.88,34.88C96.88,35.59 96.3,36.17 95.58,36.17L87.83,36.17C87.12,36.17 86.54,35.59 86.54,34.88ZM99.46,34.88C99.46,34.16 100.04,33.58 100.75,33.58H108.5C109.21,33.58 109.79,34.16 109.79,34.88C109.79,35.59 109.21,36.17 108.5,36.17L100.75,36.17C100.04,36.17 99.46,35.59 99.46,34.88ZM99.46,42.63C99.46,41.91 100.04,41.33 100.75,41.33L108.5,41.33C109.21,41.33 109.79,41.91 109.79,42.63C109.79,43.34 109.21,43.92 108.5,43.92L100.75,43.92C100.04,43.92 99.46,43.34 99.46,42.63ZM100.75,49.08C100.04,49.08 99.46,49.66 99.46,50.38C99.46,51.09 100.04,51.67 100.75,51.67H108.5C109.21,51.67 109.79,51.09 109.79,50.38C109.79,49.66 109.21,49.08 108.5,49.08L100.75,49.08ZM99.46,58.13C99.46,57.41 100.04,56.83 100.75,56.83H108.5C109.21,56.83 109.79,57.41 109.79,58.13C109.79,58.84 109.21,59.42 108.5,59.42H100.75C100.04,59.42 99.46,58.84 99.46,58.13ZM100.75,64.58C100.04,64.58 99.46,65.16 99.46,65.88C99.46,66.59 100.04,67.17 100.75,67.17H108.5C109.21,67.17 109.79,66.59 109.79,65.88C109.79,65.16 109.21,64.58 108.5,64.58H100.75ZM87.83,41.33C87.12,41.33 86.54,41.91 86.54,42.63C86.54,43.34 87.12,43.92 87.83,43.92L95.58,43.92C96.3,43.92 96.88,43.34 96.88,42.63C96.88,41.91 96.3,41.33 95.58,41.33L87.83,41.33ZM86.54,50.38C86.54,49.66 87.12,49.08 87.83,49.08L95.58,49.08C96.3,49.08 96.88,49.66 96.88,50.38C96.88,51.09 96.3,51.67 95.58,51.67H87.83C87.12,51.67 86.54,51.09 86.54,50.38ZM87.83,56.83C87.12,56.83 86.54,57.41 86.54,58.13C86.54,58.84 87.12,59.42 87.83,59.42H95.58C96.3,59.42 96.88,58.84 96.88,58.13C96.88,57.41 96.3,56.83 95.58,56.83H87.83ZM86.54,65.88C86.54,65.16 87.12,64.58 87.83,64.58H95.58C96.3,64.58 96.88,65.16 96.88,65.88C96.88,66.59 96.3,67.17 95.58,67.17H87.83C87.12,67.17 86.54,66.59 86.54,65.88ZM74.92,41.33C74.2,41.33 73.63,41.91 73.63,42.63C73.63,43.34 74.2,43.92 74.92,43.92L82.67,43.92C83.38,43.92 83.96,43.34 83.96,42.63C83.96,41.91 83.38,41.33 82.67,41.33L74.92,41.33ZM60.71,42.63C60.71,41.91 61.29,41.33 62,41.33L69.75,41.33C70.46,41.33 71.04,41.91 71.04,42.63C71.04,43.34 70.46,43.92 69.75,43.92L62,43.92C61.29,43.92 60.71,43.34 60.71,42.63ZM74.92,49.08C74.2,49.08 73.63,49.66 73.63,50.38C73.63,51.09 74.2,51.67 74.92,51.67H82.67C83.38,51.67 83.96,51.09 83.96,50.38C83.96,49.66 83.38,49.08 82.67,49.08L74.92,49.08ZM73.63,58.13C73.63,57.41 74.2,56.83 74.92,56.83H82.67C83.38,56.83 83.96,57.41 83.96,58.13C83.96,58.84 83.38,59.42 82.67,59.42H74.92C74.2,59.42 73.63,58.84 73.63,58.13ZM74.92,64.58C74.2,64.58 73.63,65.16 73.63,65.88C73.63,66.59 74.2,67.17 74.92,67.17H82.67C83.38,67.17 83.96,66.59 83.96,65.88C83.96,65.16 83.38,64.58 82.67,64.58H74.92Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@@ -1138,17 +1138,10 @@ Do you want to switch to this account?</string>
<string name="copy_email">Copy email</string>
<string name="copy_phone">Copy phone number</string>
<string name="copy_address">Copy address</string>
<string name="important_notice">Important notice</string>
<string name="bitwarden_will_soon_send_a_code_to_your_account_email_to_verify_logins_from_new_devices_in_february">Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025.</string>
<string name="do_you_have_reliable_access_to_your_email">Do you have reliable access to your email, <annotation emphasis="bold"><annotation arg="0">%1$s?</annotation></annotation>?</string>
<string name="yes_i_can_reliably_access_my_email">Yes, I can reliably access my email</string>
<string name="biometrics_no_longer_supported_title">Biometrics are no longer supported on this device</string>
<string name="biometrics_no_longer_supported">Youve been logged out because your devices biometrics dont meet the latest security requirements. To update settings, log in once again or contact your administrator for access.</string>
<string name="cxp_import">CXP Import</string>
<string name="cxp_export">CXP Export</string>
<string name="set_up_two_step_login">Set up two-step login</string>
<string name="you_can_set_up_two_step_login_as_an_alternative_way_to_protect_your_account_or_change_your_email_to_one_you_can_access">You can set up two-step login as an alternative way to protect your account or change your email to one you can access.</string>
<string name="turn_on_two_step_login">Turn on two-step login</string>
<string name="change_account_email">Change account email</string>
<string name="choose_three_or_four_random_words">Choose three or four random words</string>
<string name="pick_three_or_four_random_unrelated_words">Pick three or four random, unrelated words that you can easily remember. Think of <annotation emphasis="bold">objects, places, or things</annotation> you like.</string>
@@ -1162,7 +1155,6 @@ Do you want to switch to this account?</string>
<string name="we_couldnt_verify_the_servers_certificate">We couldnt verify the servers certificate. The certificate chain or proxy settings on your device or your Bitwarden server may not be set up correctly.</string>
<string name="review_flow_launched">Review flow launched!</string>
<string name="copy_private_key">Copy private key</string>
<string name="you_can_change_your_account_email_on_the_bitwarden_web_app">You can change your account email on the Bitwarden web app.</string>
<string name="login_credentials">Login Credentials</string>
<string name="autofill_options">Autofill Options</string>
<string name="use_this_button_to_generate_a_new_unique_password">Use this button to generate a new unique password.</string>

View File

@@ -10,8 +10,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@@ -1278,64 +1276,6 @@ class AuthDiskSourceTest {
}
}
@Test
fun `getNewDeviceNoticeState should pull from SharedPreferences`() {
val storeKey = "bwPreferencesStorage:newDeviceNoticeState"
val mockUserId = "mockUserId"
val expectedState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.parse("2024-12-25T01:00:00.00Z"),
)
fakeSharedPreferences.edit {
putString(
"${storeKey}_$mockUserId",
json.encodeToString(expectedState),
)
}
val actual = authDiskSource.getNewDeviceNoticeState(userId = mockUserId)
assertEquals(
expectedState,
actual,
)
}
@Test
fun `getNewDeviceNoticeState should pull default from SharedPreferences if no user is found`() {
val mockUserId = "mockUserId"
val defaultState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
val actual = authDiskSource.getNewDeviceNoticeState(userId = mockUserId)
assertEquals(
defaultState,
actual,
)
}
@Test
fun `setNewDeviceNoticeState should update SharedPreferences`() {
val storeKey = "bwPreferencesStorage:newDeviceNoticeState"
val mockUserId = "mockUserId"
val mockStatus = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.parse("2024-12-25T01:00:00.00Z"),
)
authDiskSource.storeNewDeviceNoticeState(
userId = mockUserId,
mockStatus,
)
val actual = fakeSharedPreferences.getString(
"${storeKey}_$mockUserId",
null,
)
assertEquals(
json.encodeToString(mockStatus),
actual,
)
}
@Test
fun `getLastLockTimestamp should pull from SharedPreferences`() {
val storeKey = "bwPreferencesStorage:lastLockTimestamp"

View File

@@ -3,8 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.util
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@@ -64,7 +62,6 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedPolicies = mutableMapOf<String, List<SyncResponseJson.Policy>?>()
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
private val storedNewDeviceNoticeState = mutableMapOf<String, NewDeviceNoticeState?>()
private val storedLastLockTimestampState = mutableMapOf<String, Instant?>()
override var userState: UserStateJson? = null
@@ -312,17 +309,6 @@ class FakeAuthDiskSource : AuthDiskSource {
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
override fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState {
return storedNewDeviceNoticeState[userId] ?: NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
}
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
storedNewDeviceNoticeState[userId] = newState
}
override fun getLastLockTimestamp(userId: String): Instant? {
return storedLastLockTimestampState[userId]
}
@@ -482,7 +468,7 @@ class FakeAuthDiskSource : AuthDiskSource {
}
/**
* Assert that the [lastLockTimestamp] was stored successfully using the [userId].
* Assert that the last lock timestamp was stored successfully using the [userId].
*/
fun assertLastLockTimestamp(userId: String, expectedValue: Instant?) {
assertEquals(expectedValue, storedLastLockTimestampState[userId])

View File

@@ -29,8 +29,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@@ -153,7 +151,6 @@ import kotlinx.serialization.json.put
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
@@ -6601,345 +6598,6 @@ class AuthRepositoryTest {
assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1))
}
@Test
fun `getNewDeviceNoticeState should return device notice state if an account is active`() =
runTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val deviceNoticeState = repository.getNewDeviceNoticeState()
assertNotNull(deviceNoticeState)
}
@Test
fun `getNewDeviceNoticeState should return null if no account is active`() =
runTest {
val deviceNoticeState = repository.getNewDeviceNoticeState()
assertNull(deviceNoticeState)
}
@Test
fun `setNewDeviceNoticeState should update disk source`() =
runTest {
val userId = "2a135b23-e1fb-42c9-bec3-573857bc8181"
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
repository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
),
)
assertEquals(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
),
fakeAuthDiskSource.getNewDeviceNoticeState(userId),
)
}
@Test
@Suppress("MaxLineLength")
fun `setNewDeviceNoticeState without an active account should not update disk source and return default`() =
runTest {
val userId = "2a135b23-e1fb-42c9-bec3-573857bc8181"
repository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
),
)
assertEquals(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
),
fakeAuthDiskSource.getNewDeviceNoticeState(userId),
)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice flags on, is cloud user, profile at least week old, no required sso policy, no two factor enable returns true`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertTrue(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice IgnoreEnvironmentCheck flag enabled should not check for a cloud environment and return true`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.SelfHosted(
EnvironmentUrlDataJson(base = "https://myselfhosted.environment.com"),
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertTrue(shouldShowNewDeviceNotice)
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice if environment is selfhosted return false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.SelfHosted(
EnvironmentUrlDataJson(base = "https://myselfhosted.environment.com"),
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice has required SSO policy returns false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf(
createMockPolicy(
type = PolicyTypeJson.REQUIRE_SSO,
isEnabled = true,
),
)
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice with two factor enable returns false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice account less than a week old returns false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = UserStateJson(
activeUserId = USER_ID_1,
accounts = mapOf(
USER_ID_1 to ACCOUNT_1.copy(
profile = ACCOUNT_1.profile.copy(
creationDate = ZonedDateTime.now().minusDays(2),
),
),
),
)
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus CAN_ACCESS_EMAIL_PERMANENT return false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storeNewDeviceNoticeState(
userId = USER_ID_1,
newState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT,
lastSeenDate = null,
),
)
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus HAS_NOT_SEEN return true`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storeNewDeviceNoticeState(
userId = USER_ID_1,
newState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
),
)
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertTrue(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus HAS_SEEN return true if date is older than 7 days`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storeNewDeviceNoticeState(
userId = USER_ID_1,
newState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.now().minusDays(10),
),
)
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertTrue(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus HAS_SEEN return false if date is not older than 7 days`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storeNewDeviceNoticeState(
userId = USER_ID_1,
newState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.now().minusDays(2),
),
)
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus CAN_ACCESS_EMAIL return true`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storeNewDeviceNoticeState(
userId = USER_ID_1,
newState = NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL,
lastSeenDate = ZonedDateTime.now().minusDays(2),
),
)
assertTrue(repository.checkUserNeedsNewDeviceTwoFactorNotice())
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice with no active user returns false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = null
assertFalse(repository.checkUserNeedsNewDeviceTwoFactorNotice())
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice account with null creationDate returns false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = UserStateJson(
activeUserId = USER_ID_1,
accounts = mapOf(
USER_ID_1 to ACCOUNT_1.copy(
profile = ACCOUNT_1.profile.copy(
creationDate = null,
),
),
),
)
assertFalse(repository.checkUserNeedsNewDeviceTwoFactorNotice())
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice account with null isTwoFactorEnabled returns true`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.Us
fakeAuthDiskSource.userState = UserStateJson(
activeUserId = USER_ID_1,
accounts = mapOf(
USER_ID_1 to ACCOUNT_1.copy(
profile = ACCOUNT_1.profile.copy(
isTwoFactorEnabled = null,
),
),
),
)
assertTrue(repository.checkUserNeedsNewDeviceTwoFactorNotice())
}
companion object {
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val NAME = "Example Name"

View File

@@ -1,120 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.After
import org.junit.Before
import org.junit.Test
class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<NewDeviceNoticeEmailAccessEvent>()
private var onNavigateBackToVaultCalled = false
private var onNavigateToTwoFactorOptionsCalled = false
private val viewModel = mockk<NewDeviceNoticeEmailAccessViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
}
private val intentManager: IntentManager = mockk {
every { launchUri(any()) } just runs
}
@Before
fun setUp() {
setContent(
intentManager = intentManager,
) {
NewDeviceNoticeEmailAccessScreen(
onNavigateBackToVault = { onNavigateBackToVaultCalled = true },
onNavigateToTwoFactorOptions = { onNavigateToTwoFactorOptionsCalled = true },
viewModel = viewModel,
)
}
}
@After
fun tearDown() {
onNavigateBackToVaultCalled = false
onNavigateToTwoFactorOptionsCalled = false
}
@Test
@Suppress("MaxLineLength")
fun `Do you have reliable access to your email should be toggled on or off according to the state`() {
composeTestRule
.onNodeWithText("Yes, I can reliably access my email", substring = true)
.assertIsOff()
mutableStateFlow.update { it.copy(isEmailAccessEnabled = true) }
composeTestRule
.onNodeWithText("Yes, I can reliably access my email", substring = true)
.assertIsOn()
}
@Test
fun `Do you have reliable access to your email click should send EmailAccessToggle action`() {
composeTestRule
.onNodeWithText("Yes, I can reliably access my email")
.performClick()
verify {
viewModel.trySendAction(
NewDeviceNoticeEmailAccessAction.EmailAccessToggle(true),
)
}
}
@Test
fun `Continue button click should send ContinueButtonClick action`() {
composeTestRule.onNodeWithText("Continue").performScrollTo().performClick()
verify {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.ContinueClick)
}
}
@Test
fun `ContinueClick should call onNavigateBackToVault if isEmailAccessEnabled is false`() {
mutableStateFlow.update { it.copy(isEmailAccessEnabled = false) }
mutableEventFlow.tryEmit(NewDeviceNoticeEmailAccessEvent.NavigateBackToVault)
assertTrue(onNavigateBackToVaultCalled)
}
@Test
fun `ContinueClick should call onNavigateToTwoFactorOptions if isEmailAccessEnabled is true`() {
mutableStateFlow.update { it.copy(isEmailAccessEnabled = true) }
mutableEventFlow.tryEmit(NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions)
assertTrue(onNavigateToTwoFactorOptionsCalled)
}
@Test
fun `on NavigateToLearnMore should call launchUri on IntentManager`() {
mutableEventFlow.tryEmit(NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore)
verify {
intentManager.launchUri("https://bitwarden.com/help/new-device-verification/".toUri())
}
}
}
private const val EMAIL = "active@bitwarden.com"
private val DEFAULT_STATE =
NewDeviceNoticeEmailAccessState(
email = EMAIL,
isEmailAccessEnabled = false,
)

View File

@@ -1,132 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
private val authRepository = mockk<AuthRepository> {
every { getNewDeviceNoticeState() } returns NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
every { setNewDeviceNoticeState(any()) } just runs
every { checkUserNeedsNewDeviceTwoFactorNotice() } returns true
}
private val vaultRepository = mockk<VaultRepository>(relaxed = true)
@Test
fun `initial state should be correct with email from state handle`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
}
}
@Test
fun `Init should not send events if user needs new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns true
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
}
}
@Test
fun `Init should send NavigateBackToVault if user does not need new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns false
val viewModel = createViewModel()
viewModel.eventFlow.test {
assertEquals(
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault,
awaitItem(),
)
}
}
@Test
fun `EmailAccessToggle should update value of isEmailAccessEnabled`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.EmailAccessToggle(true))
assertEquals(
viewModel.stateFlow.value,
DEFAULT_STATE.copy(isEmailAccessEnabled = true),
)
}
}
@Test
fun `ContinueClick should emit NavigateBackToVault if isEmailAccessEnabled`() = runTest {
val viewModel = createViewModel()
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.EmailAccessToggle(true))
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.ContinueClick)
assertEquals(
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault,
awaitItem(),
)
verify(exactly = 1) {
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT,
lastSeenDate = null,
),
)
}
}
}
@Test
fun `ContinueClick should emit NavigateToTwoFactorOptions if isEmailAccessEnabled is false`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.ContinueClick)
assertEquals(
NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions,
awaitItem(),
)
}
}
@Test
fun `LearnMoreClick should emit NavigateToLearnMore`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.LearnMoreClick)
assertEquals(NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore, awaitItem())
}
}
private fun createViewModel(
savedStateHandle: SavedStateHandle = SavedStateHandle().also {
it["email_address"] = EMAIL
},
): NewDeviceNoticeEmailAccessViewModel = NewDeviceNoticeEmailAccessViewModel(
authRepository = authRepository,
vaultRepository = vaultRepository,
savedStateHandle = savedStateHandle,
)
}
private const val EMAIL = "active@bitwarden.com"
private val DEFAULT_STATE =
NewDeviceNoticeEmailAccessState(
email = EMAIL,
isEmailAccessEnabled = false,
)

View File

@@ -1,227 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class NewDeviceNoticeTwoFactorScreenTest : BaseComposeTest() {
private val intentManager = mockk<IntentManager>(relaxed = true) {
every { startCustomTabsActivity(any()) } just runs
}
private var onNavigateBackToVaultCalled = false
private var onNavigateBackCalled = false
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<NewDeviceNoticeTwoFactorEvent>()
private val viewModel = mockk<NewDeviceNoticeTwoFactorViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
}
@Before
fun setUp() {
setContent(
intentManager = intentManager,
) {
NewDeviceNoticeTwoFactorScreen(
onNavigateBackToVault = { onNavigateBackToVaultCalled = true },
onNavigateBack = { onNavigateBackCalled = true },
viewModel = viewModel,
)
}
}
@After
fun tearDown() {
onNavigateBackToVaultCalled = false
onNavigateBackCalled = false
}
@Test
fun `onNavigateBack should send action to viewModel`() {
composeTestRule
.onNodeWithContentDescription("Back")
.performClick()
verify {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.NavigateBackClick)
}
}
@Test
fun `Turn on two-step verification click should send TurnOnTwoFactorClick action`() {
composeTestRule
.onNodeWithText("Turn on", substring = true)
.performScrollTo()
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(
NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick,
)
}
}
@Test
fun `Change account email click should send ChangeAccountEmailClick action`() {
composeTestRule
.onNodeWithText("Change account email")
.performScrollTo()
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(
NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick,
)
}
}
@Test
fun `on NavigateToTurnOnTwoFactor should call launchUri on IntentManager`() {
mutableEventFlow.tryEmit(
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(
url = "https://bitwarden.com/#/settings/security/two-factor",
),
)
verify(exactly = 1) {
intentManager.launchUri("https://bitwarden.com/#/settings/security/two-factor".toUri())
}
}
@Test
fun `ChangeAccountEmailClick should call OnNavigateBack`() {
mutableEventFlow.tryEmit(
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(
url = "https://vault.bitwarden.com/#/settings/account",
),
)
verify(exactly = 1) {
intentManager.launchUri("https://vault.bitwarden.com/#/settings/account".toUri())
}
}
@Test
fun `RemindMeLaterClick should call OnNavigateBack`() {
mutableEventFlow.tryEmit(NewDeviceNoticeTwoFactorEvent.NavigateBackToVault)
assertTrue(onNavigateBackToVaultCalled)
}
@Test
fun `onNavigateBack should set onNavigateBackCalled to true`() {
mutableEventFlow.tryEmit(NewDeviceNoticeTwoFactorEvent.NavigateBack)
Assert.assertTrue(onNavigateBackCalled)
}
@Test
fun `turn on two factor dialog should be shown or hidden according to the state`() {
composeTestRule.onNode(isDialog()).assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = NewDeviceNoticeTwoFactorDialogState.TurnOnTwoFactorDialog,
)
}
composeTestRule.onNode(isDialog()).assertIsDisplayed()
composeTestRule
.onNodeWithText("Continue to web app", substring = true, ignoreCase = true)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
@Suppress("MaxLineLength")
composeTestRule
.onNodeWithText(
text = "Make your account more secure by setting up two-step login in the Bitwarden web app.",
substring = true,
ignoreCase = true,
)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Continue", substring = true, ignoreCase = true)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Cancel", substring = true, ignoreCase = true)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `change account email dialog should be shown or hidden according to the state`() {
composeTestRule.onNode(isDialog()).assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = NewDeviceNoticeTwoFactorDialogState.ChangeAccountEmailDialog,
)
}
composeTestRule.onNode(isDialog()).assertIsDisplayed()
composeTestRule
.onNodeWithText("Continue to web app", substring = true, ignoreCase = true)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText(
"You can change your account email on the Bitwarden web app.",
substring = true,
ignoreCase = true,
)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Continue", substring = true, ignoreCase = true)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Cancel", substring = true, ignoreCase = true)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `dialog should be hidden according to the state`() {
composeTestRule.onNode(isDialog()).assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = NewDeviceNoticeTwoFactorDialogState.ChangeAccountEmailDialog,
)
}
composeTestRule.onNode(isDialog()).assertIsDisplayed()
mutableStateFlow.update {
it.copy(
dialogState = null,
)
}
composeTestRule.onNode(isDialog()).assertDoesNotExist()
}
}
private val DEFAULT_STATE =
NewDeviceNoticeTwoFactorState(
dialogState = null,
)

View File

@@ -1,188 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorDialogState.ChangeAccountEmailDialog
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorDialogState.TurnOnTwoFactorDialog
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 NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val authRepository = mockk<AuthRepository>(relaxed = true) {
every { checkUserNeedsNewDeviceTwoFactorNotice() } returns true
}
private val settingsRepository = mockk<SettingsRepository>(relaxed = true)
private val vaultRepository = mockk<VaultRepository>(relaxed = true)
@Test
fun `initial state should be correct`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
}
}
@Test
fun `Init should not send events if user needs new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns true
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
}
}
@Test
fun `Init should send NavigateBackToVault if user does not need new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns false
val viewModel = createViewModel()
viewModel.eventFlow.test {
assertEquals(
NewDeviceNoticeTwoFactorEvent.NavigateBackToVault,
awaitItem(),
)
}
}
@Test
fun `initial state should be correct with email from state handle`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
}
}
@Test
fun `ChangeAccountEmailClick should should change dialog state to ChangeAccountEmailDialog`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick)
assertEquals(
DEFAULT_STATE.copy(dialogState = ChangeAccountEmailDialog),
viewModel.stateFlow.value,
)
}
}
@Test
fun `TurnOnTwoFactorClick should should change dialog state to TurnOnTwoFactorDialog`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick)
assertEquals(
DEFAULT_STATE.copy(dialogState = TurnOnTwoFactorDialog),
viewModel.stateFlow.value,
)
}
}
@Test
fun `DismissDialogClick should should change dialog state to null`() = runTest {
val viewModel = createViewModel()
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick)
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.DismissDialogClick)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
}
}
@Test
fun `NavigateBackClick should send NavigateBack event`() = runTest {
val viewModel = createViewModel()
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.NavigateBackClick)
viewModel.eventFlow.test {
assertEquals(
NewDeviceNoticeTwoFactorEvent.NavigateBack,
awaitItem(),
)
}
}
@Test
@Suppress("MaxLineLength")
fun `ContinueDialogClick should emit NavigateToTurnOnTwoFactor if dialog state is TurnOnTwoFactorDialog`() =
runTest {
val viewModel = createViewModel()
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick)
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.ContinueDialogClick)
assertEquals(
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(
url = "https://vault.bitwarden.com/#/settings/security/two-factor",
),
awaitItem(),
)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
verify(exactly = 1) {
settingsRepository.vaultLastSync = null
}
}
}
@Test
@Suppress("MaxLineLength")
fun `ContinueDialogClick should emit NavigateToChangeAccountEmail if dialog state is ChangeAccountEmailClick`() =
runTest {
val viewModel = createViewModel()
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick)
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.ContinueDialogClick)
assertEquals(
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(
url = "https://vault.bitwarden.com/#/settings/account",
),
awaitItem(),
)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
verify(exactly = 1) {
settingsRepository.vaultLastSync = null
}
}
}
@Test
fun `ContinueDialogClick should return if dialog state is null`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.ContinueDialogClick)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
}
}
private fun createViewModel(): NewDeviceNoticeTwoFactorViewModel =
NewDeviceNoticeTwoFactorViewModel(
authRepository = authRepository,
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
)
}
private val DEFAULT_STATE =
NewDeviceNoticeTwoFactorState(
dialogState = null,
)

View File

@@ -250,17 +250,6 @@ class RootNavScreenTest : BaseComposeTest() {
navOptions = expectedNavOptions,
)
}
// Make sure navigating to new device two factor works as expected:
rootNavStateFlow.value = RootNavState.NewDeviceTwoFactorNotice(
email = "example@bitwarden.com",
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "new_device_notice/example@bitwarden.com",
navOptions = expectedNavOptions,
)
}
}
}

View File

@@ -43,7 +43,6 @@ class RootNavViewModelTest : BaseViewModelTest() {
every { userStateFlow } returns mutableUserStateFlow
every { authStateFlow } returns mutableAuthStateFlow
every { showWelcomeCarousel } returns false
every { checkUserNeedsNewDeviceTwoFactorNotice() } returns false
}
private val mockAuthRepository = mockk<AuthRepository>(relaxed = true)
@@ -1346,45 +1345,6 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault and they need to be shown the new device notice the nav state should be NewDeviceTwoFactorNotice`() {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns true
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
needsMasterPassword = false,
trustedDevice = null,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.NewDeviceTwoFactorNotice("email"),
viewModel.stateFlow.value,
)
}
private fun createViewModel(): RootNavViewModel =
RootNavViewModel(
authRepository = authRepository,