mirror of
https://github.com/bitwarden/android.git
synced 2026-03-21 05:40:45 -05:00
PM-19645: Remove the new device UI email access flow (#4996)
This commit is contained in:
@@ -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].
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">You’ve been logged out because your device’s biometrics don’t 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 couldn’t verify the server’s 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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user