From 4d9a19f43c3f6abc3a43b0bf9a3ed82818c47e03 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 4 Apr 2025 14:41:34 -0500 Subject: [PATCH] PM-19645: Remove the new device UI email access flow (#4996) --- .../auth/datasource/disk/AuthDiskSource.kt | 11 - .../datasource/disk/AuthDiskSourceImpl.kt | 19 - .../model/NewDeviceNoticeDisplayStatus.kt | 60 --- .../data/auth/repository/AuthRepository.kt | 16 - .../auth/repository/AuthRepositoryImpl.kt | 84 ----- .../NewDeviceNoticeEmailAccessNavigation.kt | 58 --- .../NewDeviceNoticeEmailAccessScreen.kt | 215 ----------- .../NewDeviceNoticeEmailAccessViewModel.kt | 131 ------- .../NewDeviceNoticeTwoFactorNavigation.kt | 37 -- .../NewDeviceNoticeTwoFactorScreen.kt | 220 ----------- .../NewDeviceNoticeTwoFactorViewModel.kt | 203 ----------- .../platform/feature/rootnav/RootNavScreen.kt | 9 - .../feature/rootnav/RootNavViewModel.kt | 13 - .../vaultunlocked/VaultUnlockedNavigation.kt | 11 - app/src/main/res/drawable-night/user_lock.xml | 58 --- app/src/main/res/drawable-night/warning.xml | 93 ----- app/src/main/res/drawable/user_lock.xml | 58 --- app/src/main/res/drawable/warning.xml | 104 ------ app/src/main/res/values/strings.xml | 8 - .../datasource/disk/AuthDiskSourceTest.kt | 60 --- .../disk/util/FakeAuthDiskSource.kt | 16 +- .../auth/repository/AuthRepositoryTest.kt | 342 ------------------ .../NewDeviceNoticeEmailAccessScreenTest.kt | 120 ------ ...NewDeviceNoticeEmailAccessViewModelTest.kt | 132 ------- .../NewDeviceNoticeTwoFactorScreenTest.kt | 227 ------------ .../NewDeviceNoticeTwoFactorViewModelTest.kt | 188 ---------- .../feature/rootnav/RootNavScreenTest.kt | 11 - .../feature/rootnav/RootNavViewModelTest.kt | 40 -- 28 files changed, 1 insertion(+), 2543 deletions(-) delete mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/NewDeviceNoticeDisplayStatus.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessNavigation.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModel.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorNavigation.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreen.kt delete mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModel.kt delete mode 100644 app/src/main/res/drawable-night/user_lock.xml delete mode 100644 app/src/main/res/drawable-night/warning.xml delete mode 100644 app/src/main/res/drawable/user_lock.xml delete mode 100644 app/src/main/res/drawable/warning.xml delete mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreenTest.kt delete mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModelTest.kt delete mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreenTest.kt delete mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModelTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index 6f47f54a9f..73ecb364dc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -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 - /** - * 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]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 658b72d392..9243bb8124 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -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) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/NewDeviceNoticeDisplayStatus.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/NewDeviceNoticeDisplayStatus.kt deleted file mode 100644 index 2f9a1f8cae..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/NewDeviceNoticeDisplayStatus.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index 9296df66c1..75643f9f62 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -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?) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 583d230c9c..585fc31fa5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessNavigation.kt deleted file mode 100644 index 158d3b8727..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessNavigation.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt deleted file mode 100644 index d7a9f9180c..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt +++ /dev/null @@ -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 = {}, - ) - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModel.kt deleted file mode 100644 index a2f8e861dd..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModel.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorNavigation.kt deleted file mode 100644 index 8be96d9e7b..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorNavigation.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreen.kt deleted file mode 100644 index 320cb2b69f..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreen.kt +++ /dev/null @@ -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 = {}, - ) - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModel.kt deleted file mode 100644 index 2abb3b544f..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModel.kt +++ /dev/null @@ -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(), - ) -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index b26c5f3ddf..edaac32c0e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -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, - ) - } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index e5acfd5d98..92a60f4750 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -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() } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index a7766f8559..5521e1d0ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -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() }, - ) } } diff --git a/app/src/main/res/drawable-night/user_lock.xml b/app/src/main/res/drawable-night/user_lock.xml deleted file mode 100644 index bd8979feb2..0000000000 --- a/app/src/main/res/drawable-night/user_lock.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/warning.xml b/app/src/main/res/drawable-night/warning.xml deleted file mode 100644 index f173ccbf1e..0000000000 --- a/app/src/main/res/drawable-night/warning.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/user_lock.xml b/app/src/main/res/drawable/user_lock.xml deleted file mode 100644 index ccf6929b19..0000000000 --- a/app/src/main/res/drawable/user_lock.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/warning.xml b/app/src/main/res/drawable/warning.xml deleted file mode 100644 index 55bbf1f486..0000000000 --- a/app/src/main/res/drawable/warning.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8536f4b0cc..cc453b0e43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1138,17 +1138,10 @@ Do you want to switch to this account? Copy email Copy phone number Copy address - Important notice - Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025. - Do you have reliable access to your email, %1$s?? - Yes, I can reliably access my email Biometrics are no longer supported on this device 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. CXP Import CXP Export - Set up two-step login - You can set up two-step login as an alternative way to protect your account or change your email to one you can access. - Turn on two-step login Change account email Choose three or four random words Pick three or four random, unrelated words that you can easily remember. Think of objects, places, or things you like. @@ -1162,7 +1155,6 @@ Do you want to switch to this account? 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. Review flow launched! Copy private key - You can change your account email on the Bitwarden web app. Login Credentials Autofill Options Use this button to generate a new unique password. diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 7445deee47..57a18a47eb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -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" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt index 229578cffd..caef0a97d0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -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?>() private val storedOnboardingStatus = mutableMapOf() private val storedShowImportLogins = mutableMapOf() - private val storedNewDeviceNoticeState = mutableMapOf() private val storedLastLockTimestampState = mutableMapOf() 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]) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 765cb45941..d901ea07f8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -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" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreenTest.kt deleted file mode 100644 index 4ac40aa017..0000000000 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreenTest.kt +++ /dev/null @@ -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() - private var onNavigateBackToVaultCalled = false - private var onNavigateToTwoFactorOptionsCalled = false - private val viewModel = mockk(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, - ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModelTest.kt deleted file mode 100644 index 41990f1739..0000000000 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessViewModelTest.kt +++ /dev/null @@ -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 { - every { getNewDeviceNoticeState() } returns NewDeviceNoticeState( - displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN, - lastSeenDate = null, - ) - every { setNewDeviceNoticeState(any()) } just runs - every { checkUserNeedsNewDeviceTwoFactorNotice() } returns true - } - - private val vaultRepository = mockk(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, - ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreenTest.kt deleted file mode 100644 index 9fb7e78598..0000000000 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorScreenTest.kt +++ /dev/null @@ -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(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() - private val viewModel = mockk(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, - ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModelTest.kt deleted file mode 100644 index fb08e6a745..0000000000 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeTwoFactorViewModelTest.kt +++ /dev/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(relaxed = true) { - every { checkUserNeedsNewDeviceTwoFactorNotice() } returns true - } - - private val settingsRepository = mockk(relaxed = true) - - private val vaultRepository = mockk(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, - ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index b3700256b3..44fc4cf403 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -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, - ) - } } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 6bca299bc4..f50125b187 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -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(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,