mirror of
https://github.com/bitwarden/android.git
synced 2025-01-30 19:53:47 +03:00
[PM-8217] New device two factor notice (#4508)
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
parent
ae8db9256c
commit
a35ec8cf3c
29 changed files with 1343 additions and 51 deletions
app/src
main
java/com/x8bit/bitwarden
data
auth
datasource/disk
repository
platform/manager/model
ui
auth/feature/newdevicenotice
NewDeviceNoticeEmailAccessNavigation.ktNewDeviceNoticeEmailAccessScreen.ktNewDeviceNoticeEmailAccessViewModel.ktNewDeviceNoticeTwoFactorNavigation.ktNewDeviceNoticeTwoFactorScreen.ktNewDeviceNoticeTwoFactorViewModel.kt
platform/feature
debugmenu/components
rootnav
vaultunlocked
res/values
test/java/com/x8bit/bitwarden
data
auth
platform/manager
ui
auth/feature/newdevicenotice
NewDeviceNoticeEmailAccessScreenTest.ktNewDeviceNoticeEmailAccessViewModelTest.ktNewDeviceNoticeTwoFactorScreenTest.ktNewDeviceNoticeTwoFactorViewModelTest.kt
platform/feature
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
@ -328,7 +329,17 @@ interface AuthDiskSource {
|
|||
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
|
||||
* Emits updates that track [getShowImportLogins]. This will replay the last known value.
|
||||
*/
|
||||
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the new device notice state for the given [userId].
|
||||
*/
|
||||
fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState
|
||||
|
||||
/**
|
||||
* Stores the new device notice state for the given [userId].
|
||||
*/
|
||||
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
|
|||
|
||||
import android.content.SharedPreferences
|
||||
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
|
||||
|
@ -46,6 +48,7 @@ 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"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
|
@ -471,6 +474,22 @@ 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) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Describes the current display status of the new device notice screen.
|
||||
*/
|
||||
@Serializable
|
||||
enum class NewDeviceNoticeDisplayStatus {
|
||||
/**
|
||||
* The user has seen the screen and indicated they can access their email.
|
||||
*/
|
||||
@SerialName("canAccessEmail")
|
||||
CAN_ACCESS_EMAIL,
|
||||
|
||||
/**
|
||||
* The user has indicated they can access their email
|
||||
* as specified by the Permanent mode of the notice.
|
||||
*/
|
||||
@SerialName("canAccessEmailPermanent")
|
||||
CAN_ACCESS_EMAIL_PERMANENT,
|
||||
|
||||
/**
|
||||
* The user has not seen the screen.
|
||||
*/
|
||||
@SerialName("hasNotSeen")
|
||||
HAS_NOT_SEEN,
|
||||
|
||||
/**
|
||||
* The user has seen the screen and selected "remind me later".
|
||||
*/
|
||||
@SerialName("hasSeen")
|
||||
HAS_SEEN,
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the new device notice screen.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class NewDeviceNoticeState(
|
||||
@SerialName("displayStatus")
|
||||
val displayStatus: NewDeviceNoticeDisplayStatus,
|
||||
|
||||
@SerialName("lastSeenDate")
|
||||
@Contextual
|
||||
val lastSeenDate: ZonedDateTime?,
|
||||
) {
|
||||
/**
|
||||
* Whether the [lastSeenDate] is at least 7 days old.
|
||||
*/
|
||||
val shouldDisplayNoticeIfSeen = lastSeenDate
|
||||
?.isBefore(
|
||||
ZonedDateTime.now().minusDays(7),
|
||||
)
|
||||
?: false
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
@ -401,4 +402,19 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(userId: String, 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?)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ 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
|
||||
|
@ -106,6 +108,7 @@ 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.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
@ -141,6 +144,7 @@ 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
|
||||
|
||||
/**
|
||||
|
@ -1333,6 +1337,89 @@ class AuthRepositoryImpl(
|
|||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
}
|
||||
|
||||
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 ->
|
||||
val temporaryFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
val permanentFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
|
||||
// check if feature flags are disabled
|
||||
if (!temporaryFlag && !permanentFlag) {
|
||||
return false
|
||||
}
|
||||
|
||||
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 -> permanentFlag
|
||||
// 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 {
|
||||
if (environmentRepository.environment.type == Environment.Type.SELF_HOSTED) {
|
||||
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,
|
||||
|
|
|
@ -36,6 +36,8 @@ sealed class FlagKey<out T : Any> {
|
|||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
AppReviewPrompt,
|
||||
NewDevicePermanentDismiss,
|
||||
NewDeviceTemporaryDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +143,24 @@ sealed class FlagKey<out T : Any> {
|
|||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the New Device Temporary Dismiss feature.
|
||||
*/
|
||||
data object NewDeviceTemporaryDismiss : FlagKey<Boolean>() {
|
||||
override val keyName: String = "new-device-temporary-dismiss"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the New Device Permanent Dismiss feature.
|
||||
*/
|
||||
data object NewDevicePermanentDismiss : FlagKey<Boolean>() {
|
||||
override val keyName: String = "new-device-permanent-dismiss"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
|
|
|
@ -41,6 +41,7 @@ fun NavController.navigateToNewDeviceNoticeEmailAccess(
|
|||
* Add the new device notice email access screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.newDeviceNoticeEmailAccessDestination(
|
||||
onNavigateBackToVault: () -> Unit,
|
||||
onNavigateToTwoFactorOptions: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
|
@ -50,6 +51,7 @@ fun NavGraphBuilder.newDeviceNoticeEmailAccessDestination(
|
|||
),
|
||||
) {
|
||||
NewDeviceNoticeEmailAccessScreen(
|
||||
onNavigateBackToVault = onNavigateBackToVault,
|
||||
onNavigateToTwoFactorOptions = onNavigateToTwoFactorOptions,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
|||
*/
|
||||
@Composable
|
||||
fun NewDeviceNoticeEmailAccessScreen(
|
||||
onNavigateBackToVault: () -> Unit,
|
||||
onNavigateToTwoFactorOptions: () -> Unit,
|
||||
viewModel: NewDeviceNoticeEmailAccessViewModel = hiltViewModel(),
|
||||
) {
|
||||
|
@ -51,6 +52,7 @@ fun NewDeviceNoticeEmailAccessScreen(
|
|||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
NavigateToTwoFactorOptions -> onNavigateToTwoFactorOptions()
|
||||
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault -> onNavigateBackToVault()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,12 @@ package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL
|
||||
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.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
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.NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions
|
||||
|
@ -9,6 +15,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
@ -18,6 +25,8 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<
|
||||
NewDeviceNoticeEmailAccessState,
|
||||
|
@ -38,8 +47,24 @@ class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
// TODO PM-8217: update new device notice status and navigate accordingly
|
||||
sendEvent(NavigateToTwoFactorOptions)
|
||||
if (state.isEmailAccessEnabled) {
|
||||
val displayStatus =
|
||||
if (featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)) {
|
||||
CAN_ACCESS_EMAIL_PERMANENT
|
||||
} else {
|
||||
CAN_ACCESS_EMAIL
|
||||
}
|
||||
|
||||
authRepository.setNewDeviceNoticeState(
|
||||
NewDeviceNoticeState(
|
||||
displayStatus = displayStatus,
|
||||
lastSeenDate = ZonedDateTime.now(),
|
||||
),
|
||||
)
|
||||
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateBackToVault)
|
||||
} else {
|
||||
sendEvent(NavigateToTwoFactorOptions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEmailAccessToggle(action: EmailAccessToggle) {
|
||||
|
@ -66,6 +91,11 @@ sealed class NewDeviceNoticeEmailAccessEvent {
|
|||
* Navigates to the Two Factor Options screen.
|
||||
*/
|
||||
data object NavigateToTwoFactorOptions : NewDeviceNoticeEmailAccessEvent()
|
||||
|
||||
/**
|
||||
* Navigates back.
|
||||
*/
|
||||
data object NavigateBackToVault : NewDeviceNoticeEmailAccessEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,13 +23,13 @@ fun NavController.navigateToNewDeviceNoticeTwoFactor(
|
|||
* Add the new device notice two factor screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.newDeviceNoticeTwoFactorDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateBackToVault: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = NEW_DEVICE_NOTICE_TWO_FACTOR_ROUTE,
|
||||
) {
|
||||
NewDeviceNoticeTwoFactorScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateBackToVault = onNavigateBackToVault,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ 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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -21,17 +22,21 @@ 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.RemindMeLaterClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.TurnOnTwoFactorClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorEvent.NavigateBack
|
||||
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.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
|
||||
|
@ -43,10 +48,11 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
|||
*/
|
||||
@Composable
|
||||
fun NewDeviceNoticeTwoFactorScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateBackToVault: () -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: NewDeviceNoticeTwoFactorViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is NavigateToTurnOnTwoFactor -> {
|
||||
|
@ -57,10 +63,28 @@ fun NewDeviceNoticeTwoFactorScreen(
|
|||
intentManager.launchUri(event.url.toUri())
|
||||
}
|
||||
|
||||
NavigateBack -> onNavigateBack()
|
||||
NavigateBackToVault -> onNavigateBackToVault()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
BitwardenScaffold {
|
||||
NewDeviceNoticeTwoFactorContent(
|
||||
onTurnOnTwoFactorClick = {
|
||||
|
@ -72,6 +96,7 @@ fun NewDeviceNoticeTwoFactorScreen(
|
|||
onRemindMeLaterClick = {
|
||||
viewModel.trySendAction(RemindMeLaterClick)
|
||||
},
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +109,7 @@ private fun NewDeviceNoticeTwoFactorContent(
|
|||
onTurnOnTwoFactorClick: () -> Unit,
|
||||
onChangeAccountEmailClick: () -> Unit,
|
||||
onRemindMeLaterClick: () -> Unit,
|
||||
state: NewDeviceNoticeTwoFactorState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
@ -100,6 +126,7 @@ private fun NewDeviceNoticeTwoFactorContent(
|
|||
onTurnOnTwoFactorClick = onTurnOnTwoFactorClick,
|
||||
onChangeAccountEmailClick = onChangeAccountEmailClick,
|
||||
onRemindMeLaterClick = onRemindMeLaterClick,
|
||||
state = state,
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
|
@ -142,6 +169,7 @@ private fun ColumnScope.MainContent(
|
|||
onTurnOnTwoFactorClick: () -> Unit,
|
||||
onChangeAccountEmailClick: () -> Unit,
|
||||
onRemindMeLaterClick: () -> Unit,
|
||||
state: NewDeviceNoticeTwoFactorState,
|
||||
) {
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.turn_on_two_step_login),
|
||||
|
@ -158,13 +186,15 @@ private fun ColumnScope.MainContent(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(R.string.remind_me_later),
|
||||
onClick = onRemindMeLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
if (state.shouldShowRemindMeLater) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(R.string.remind_me_later),
|
||||
onClick = onRemindMeLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreenSizes
|
||||
|
@ -175,6 +205,9 @@ private fun NewDeviceNoticeTwoFactorScreen_preview() {
|
|||
onTurnOnTwoFactorClick = {},
|
||||
onChangeAccountEmailClick = {},
|
||||
onRemindMeLaterClick = {},
|
||||
state = NewDeviceNoticeTwoFactorState(
|
||||
shouldShowRemindMeLater = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.x8bit.bitwarden.R
|
||||
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.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
|
||||
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.RemindMeLaterClick
|
||||
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.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -14,9 +29,19 @@ import javax.inject.Inject
|
|||
*/
|
||||
@HiltViewModel
|
||||
class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
|
||||
val authRepository: AuthRepository,
|
||||
val environmentRepository: EnvironmentRepository,
|
||||
) : BaseViewModel<Unit, NewDeviceNoticeTwoFactorEvent, NewDeviceNoticeTwoFactorAction>(
|
||||
initialState = Unit,
|
||||
val featureFlagManager: FeatureFlagManager,
|
||||
) : BaseViewModel<
|
||||
NewDeviceNoticeTwoFactorState,
|
||||
NewDeviceNoticeTwoFactorEvent,
|
||||
NewDeviceNoticeTwoFactorAction,
|
||||
>(
|
||||
initialState = NewDeviceNoticeTwoFactorState(
|
||||
shouldShowRemindMeLater = !featureFlagManager.getFeatureFlag(
|
||||
FlagKey.NewDevicePermanentDismiss,
|
||||
),
|
||||
),
|
||||
) {
|
||||
private val webTwoFactorUrl: String
|
||||
get() {
|
||||
|
@ -38,22 +63,51 @@ class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
|
|||
|
||||
override fun handleAction(action: NewDeviceNoticeTwoFactorAction) {
|
||||
when (action) {
|
||||
ChangeAccountEmailClick -> sendEvent(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(
|
||||
url = webAccountUrl,
|
||||
),
|
||||
)
|
||||
ChangeAccountEmailClick -> updateDialogState(newState = ChangeAccountEmailDialog)
|
||||
|
||||
TurnOnTwoFactorClick -> sendEvent(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(
|
||||
url = webTwoFactorUrl,
|
||||
),
|
||||
)
|
||||
TurnOnTwoFactorClick -> updateDialogState(newState = TurnOnTwoFactorDialog)
|
||||
|
||||
RemindMeLaterClick -> {
|
||||
// TODO PM-8217: Add logic to remind me later
|
||||
sendEvent(NewDeviceNoticeTwoFactorEvent.NavigateBack)
|
||||
RemindMeLaterClick -> handleRemindMeLater()
|
||||
|
||||
DismissDialogClick -> updateDialogState(newState = null)
|
||||
|
||||
ContinueDialogClick -> handleContinueDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRemindMeLater() {
|
||||
authRepository.setNewDeviceNoticeState(
|
||||
NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
|
||||
lastSeenDate = null,
|
||||
),
|
||||
)
|
||||
sendEvent(NewDeviceNoticeTwoFactorEvent.NavigateBackToVault)
|
||||
}
|
||||
|
||||
private fun handleContinueDialog() {
|
||||
when (state.dialogState) {
|
||||
is ChangeAccountEmailDialog -> {
|
||||
sendEvent(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(url = webAccountUrl),
|
||||
)
|
||||
updateDialogState(newState = null)
|
||||
}
|
||||
|
||||
is TurnOnTwoFactorDialog -> {
|
||||
sendEvent(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(url = webTwoFactorUrl),
|
||||
)
|
||||
updateDialogState(newState = null)
|
||||
}
|
||||
|
||||
null -> return
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDialogState(newState: NewDeviceNoticeTwoFactorDialogState?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,9 +129,9 @@ sealed class NewDeviceNoticeTwoFactorEvent {
|
|||
data class NavigateToChangeAccountEmail(val url: String) : NewDeviceNoticeTwoFactorEvent()
|
||||
|
||||
/**
|
||||
* Navigates back.
|
||||
* Navigates back to vault.
|
||||
*/
|
||||
data object NavigateBack : NewDeviceNoticeTwoFactorEvent()
|
||||
data object NavigateBackToVault : NewDeviceNoticeTwoFactorEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,4 +152,46 @@ sealed class NewDeviceNoticeTwoFactorAction {
|
|||
* User tapped the remind me later button.
|
||||
*/
|
||||
data object RemindMeLaterClick : NewDeviceNoticeTwoFactorAction()
|
||||
|
||||
/**
|
||||
* User tapped the dismiss dialog button.
|
||||
*/
|
||||
data object DismissDialogClick : NewDeviceNoticeTwoFactorAction()
|
||||
|
||||
/**
|
||||
* User tapped the continue dialog button.
|
||||
*/
|
||||
data object ContinueDialogClick : NewDeviceNoticeTwoFactorAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state of the new device notice two factor screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class NewDeviceNoticeTwoFactorState(
|
||||
val dialogState: NewDeviceNoticeTwoFactorDialogState? = null,
|
||||
val shouldShowRemindMeLater: Boolean,
|
||||
) : 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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
|||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.AppReviewPrompt,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.NewDevicePermanentDismiss,
|
||||
FlagKey.NewDeviceTemporaryDismiss,
|
||||
-> BooleanFlagItem(
|
||||
label = flagKey.getDisplayLabel(),
|
||||
key = flagKey as FlagKey<Boolean>,
|
||||
|
@ -81,4 +83,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
|||
FlagKey.CredentialExchangeProtocolExport -> stringResource(R.string.cxp_export)
|
||||
FlagKey.AppReviewPrompt -> stringResource(R.string.app_review_prompt)
|
||||
FlagKey.CipherKeyEncryption -> stringResource(R.string.cipher_key_encryption)
|
||||
FlagKey.NewDevicePermanentDismiss -> stringResource(R.string.new_device_permanent_dismiss)
|
||||
FlagKey.NewDeviceTemporaryDismiss -> stringResource(R.string.new_device_temporary_dismiss)
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ 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.removepassword.REMOVE_PASSWORD_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword
|
||||
import com.x8bit.bitwarden.ui.auth.feature.removepassword.removePasswordDestination
|
||||
|
@ -126,6 +127,7 @@ 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
|
||||
|
@ -254,6 +256,13 @@ fun RootNavScreen(
|
|||
RootNavState.OnboardingStepsComplete -> {
|
||||
navController.navigateToSetupCompleteScreen(rootNavOptions)
|
||||
}
|
||||
|
||||
is RootNavState.NewDeviceTwoFactorNotice -> {
|
||||
navController.navigateToNewDeviceNoticeEmailAccess(
|
||||
emailAddress = currentState.email,
|
||||
navOptions = rootNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,6 +103,9 @@ class RootNavViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
userState.activeAccount.isVaultUnlocked &&
|
||||
authRepository.checkUserNeedsNewDeviceTwoFactorNotice() -> RootNavState.NewDeviceTwoFactorNotice(userState.activeAccount.email)
|
||||
|
||||
userState.activeAccount.isVaultUnlocked -> {
|
||||
when (specialCircumstance) {
|
||||
is SpecialCircumstance.AutofillSave -> {
|
||||
|
@ -367,6 +370,14 @@ sealed class RootNavState : Parcelable {
|
|||
*/
|
||||
@Parcelize
|
||||
data object OnboardingStepsComplete : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show the new device two factor notice screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class NewDeviceTwoFactorNotice(
|
||||
val email: String,
|
||||
) : RootNavState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,9 @@ 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
|
||||
|
@ -232,5 +235,12 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||
importLoginsScreenDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
newDeviceNoticeEmailAccessDestination(
|
||||
onNavigateBackToVault = { navController.navigateToVaultUnlockedGraph() },
|
||||
onNavigateToTwoFactorOptions = { navController.navigateToNewDeviceNoticeTwoFactor() },
|
||||
)
|
||||
newDeviceNoticeTwoFactorDestination(
|
||||
onNavigateBackToVault = { navController.navigateToVaultUnlockedGraph() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1111,4 +1111,5 @@ Do you want to switch to this account?</string>
|
|||
<string name="we_couldnt_verify_the_servers_certificate">We couldn’t verify the server’s certificate. The certificate chain or proxy settings on your device or your Bitwarden server may not be set up correctly.</string>
|
||||
<string name="review_flow_launched">Review flow launched!</string>
|
||||
<string name="copy_private_key">Copy private key</string>
|
||||
<string name="you_can_change_your_account_email_on_the_bitwarden_web_app">You can change your account email on the Bitwarden web app.</string>
|
||||
</resources>
|
||||
|
|
|
@ -22,5 +22,7 @@
|
|||
<string name="restart_onboarding_carousel_details">This will force the change to app state which will cause the first time carousel to show. The carousel will continue to show for any \"new\" account until a login is completed. May need to exit debug menu manually.</string>
|
||||
<string name="app_review_prompt">App Review Prompt</string>
|
||||
<string name="cipher_key_encryption">Cipher Key Encryption</string>">
|
||||
<string name="new_device_permanent_dismiss">New device notice permanent dismiss</string>">
|
||||
<string name="new_device_temporary_dismiss">New device notice temporary dismiss</string>">
|
||||
<!-- /Debug Menu -->
|
||||
</resources>
|
||||
|
|
|
@ -7,6 +7,8 @@ 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
|
||||
|
@ -1246,6 +1248,64 @@ class AuthDiskSourceTest {
|
|||
assertTrue(awaitItem() ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_STATE_JSON = """
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.util
|
|||
|
||||
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
|
||||
|
@ -60,6 +62,7 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
private val storedPolicies = mutableMapOf<String, List<SyncResponseJson.Policy>?>()
|
||||
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
|
||||
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
|
||||
private val storedNewDeviceNoticeState = mutableMapOf<String, NewDeviceNoticeState?>()
|
||||
|
||||
override var userState: UserStateJson? = null
|
||||
set(value) {
|
||||
|
@ -298,6 +301,17 @@ 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the the [isTdeLoginComplete] was stored successfully using the [userId].
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,8 @@ 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
|
||||
|
@ -147,6 +149,7 @@ 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
|
||||
|
@ -6468,6 +6471,408 @@ 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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 NewDeviceTemporaryDismiss and NewDevicePermanentDismiss flags are off returns false`() =
|
||||
runTest {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns false
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns false
|
||||
every {
|
||||
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
|
||||
} returns listOf()
|
||||
fakeEnvironmentRepository.environment = Environment.Us
|
||||
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
|
||||
|
||||
assertFalse(shouldShowNewDeviceNotice)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checkUserNeedsNewDeviceTwoFactorNotice has required SSO policy returns false`() =
|
||||
runTest {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 permanent flag value`() =
|
||||
runTest {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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())
|
||||
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns false
|
||||
|
||||
assertFalse(repository.checkUserNeedsNewDeviceTwoFactorNotice())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checkUserNeedsNewDeviceTwoFactorNotice with no active user returns false`() =
|
||||
runTest {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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 {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
} returns true
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
} returns true
|
||||
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"
|
||||
|
@ -6596,7 +7001,7 @@ class AuthRepositoryTest {
|
|||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
|
@ -6623,6 +7028,17 @@ class AuthRepositoryTest {
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val SINGLE_USER_STATE_1_NEW_ACCOUNT = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1.copy(
|
||||
profile = ACCOUNT_1.profile.copy(
|
||||
creationDate = ZonedDateTime.parse("2024-09-14T01:00:00.00Z"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_2 = UserStateJson(
|
||||
activeUserId = USER_ID_2,
|
||||
accounts = mapOf(
|
||||
|
|
|
@ -46,4 +46,24 @@ class FlagKeyTest {
|
|||
fun `AppReviewPrompt default value should be false`() {
|
||||
assertFalse(FlagKey.AppReviewPrompt.defaultValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NewDevicePermanentDismiss default value should be false`() {
|
||||
assertFalse(FlagKey.NewDevicePermanentDismiss.defaultValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NewDevicePermanentDismiss is remotely configured value should be true`() {
|
||||
assertTrue(FlagKey.NewDevicePermanentDismiss.isRemotelyConfigured)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NewDeviceTemporaryDismiss default value should be false`() {
|
||||
assertFalse(FlagKey.NewDeviceTemporaryDismiss.defaultValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NewDeviceTemporaryDismiss is remotely configured value should be true`() {
|
||||
assertTrue(FlagKey.NewDeviceTemporaryDismiss.isRemotelyConfigured)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,14 @@ import io.mockk.verify
|
|||
import junit.framework.TestCase.assertTrue
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<NewDeviceNoticeEmailAccessEvent>()
|
||||
private var onNavigateBackToVaultCalled = false
|
||||
private var onNavigateToTwoFactorOptionsCalled = false
|
||||
private val viewModel = mockk<NewDeviceNoticeEmailAccessViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
|
@ -29,14 +31,21 @@ class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
|
|||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
NewDeviceNoticeEmailAccessScreen(
|
||||
onNavigateBackToVault = { onNavigateBackToVaultCalled = true },
|
||||
onNavigateToTwoFactorOptions = { onNavigateToTwoFactorOptionsCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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)
|
||||
|
@ -70,7 +79,15 @@ class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClick should call onNavigateToTwoFactorOptions`() {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -2,12 +2,34 @@ 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.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
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 kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { getNewDeviceNoticeState() } returns NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
|
||||
lastSeenDate = null,
|
||||
)
|
||||
every { setNewDeviceNoticeState(any()) } just runs
|
||||
every { checkUserNeedsNewDeviceTwoFactorNotice() } returns true
|
||||
}
|
||||
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.NewDevicePermanentDismiss) } returns true
|
||||
every { getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss) } returns true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct with email from state handle`() = runTest {
|
||||
|
@ -30,7 +52,37 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClick should emit NavigateToTwoFactorOptions`() = runTest {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `ContinueClick should emit NavigateBackToVault if isEmailAccessEnabled and NewDevicePermanentDismiss flag is off`() = runTest {
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss) } returns false
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.EmailAccessToggle(true))
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.ContinueClick)
|
||||
assertEquals(
|
||||
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `ContinueClick should emit NavigateToTwoFactorOptions if isEmailAccessEnabled is false`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.ContinueClick)
|
||||
|
@ -46,6 +98,8 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
|
|||
it["email_address"] = EMAIL
|
||||
},
|
||||
): NewDeviceNoticeEmailAccessViewModel = NewDeviceNoticeEmailAccessViewModel(
|
||||
authRepository = authRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
|
@ -13,6 +17,8 @@ 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
|
||||
|
@ -22,8 +28,10 @@ class NewDeviceNoticeTwoFactorScreenTest : BaseComposeTest() {
|
|||
every { startCustomTabsActivity(any()) } just runs
|
||||
}
|
||||
private var onNavigateBackCalled = false
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<NewDeviceNoticeTwoFactorEvent>()
|
||||
private val viewModel = mockk<NewDeviceNoticeTwoFactorViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
}
|
||||
|
||||
|
@ -31,7 +39,7 @@ class NewDeviceNoticeTwoFactorScreenTest : BaseComposeTest() {
|
|||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
NewDeviceNoticeTwoFactorScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateBackToVault = { onNavigateBackCalled = true },
|
||||
intentManager,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
|
@ -108,7 +116,118 @@ class NewDeviceNoticeTwoFactorScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `RemindMeLaterClick should call OnNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(NewDeviceNoticeTwoFactorEvent.NavigateBack)
|
||||
mutableEventFlow.tryEmit(NewDeviceNoticeTwoFactorEvent.NavigateBackToVault)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remind me later button visibility should update according to state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Remind me later")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowRemindMeLater = false)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Remind me later")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
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()
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"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(
|
||||
shouldShowRemindMeLater = true,
|
||||
dialogState = null,
|
||||
)
|
||||
|
|
|
@ -1,39 +1,101 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
|
||||
|
||||
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.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
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.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
|
||||
private val environmentRepository = FakeEnvironmentRepository()
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { getNewDeviceNoticeState() } returns NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
|
||||
lastSeenDate = null,
|
||||
)
|
||||
every { setNewDeviceNoticeState(any()) } just runs
|
||||
}
|
||||
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.NewDevicePermanentDismiss) } returns false
|
||||
every { getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss) } returns true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ChangeAccountEmailClick should emit NavigateToChangeAccountEmail`() = runTest {
|
||||
fun `initial state should be correct with NewDevicePermanentDismiss flag false`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct with NewDevicePermanentDismiss flag true`() = runTest {
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss) } returns true
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(
|
||||
url = "https://vault.bitwarden.com/#/settings/account",
|
||||
),
|
||||
DEFAULT_STATE.copy(shouldShowRemindMeLater = false),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TurnOnTwoFactorClick should emit NavigateToTurnOnTwoFactor`() = runTest {
|
||||
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.TurnOnTwoFactorClick)
|
||||
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.DismissDialogClick)
|
||||
assertEquals(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(
|
||||
url = "https://vault.bitwarden.com/#/settings/security/two-factor",
|
||||
),
|
||||
awaitItem(),
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -44,14 +106,77 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
|
|||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(NewDeviceNoticeTwoFactorAction.RemindMeLaterClick)
|
||||
assertEquals(
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateBack,
|
||||
NewDeviceNoticeTwoFactorEvent.NavigateBackToVault,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
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,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE =
|
||||
NewDeviceNoticeTwoFactorState(
|
||||
shouldShowRemindMeLater = true,
|
||||
dialogState = null,
|
||||
)
|
||||
|
|
|
@ -117,6 +117,8 @@ private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
|||
FlagKey.CredentialExchangeProtocolImport to true,
|
||||
FlagKey.CredentialExchangeProtocolExport to true,
|
||||
FlagKey.AppReviewPrompt to true,
|
||||
FlagKey.NewDeviceTemporaryDismiss to true,
|
||||
FlagKey.NewDevicePermanentDismiss to true,
|
||||
)
|
||||
|
||||
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||
|
@ -130,6 +132,8 @@ private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
|||
FlagKey.CredentialExchangeProtocolImport to false,
|
||||
FlagKey.CredentialExchangeProtocolExport to false,
|
||||
FlagKey.AppReviewPrompt to false,
|
||||
FlagKey.NewDeviceTemporaryDismiss to false,
|
||||
FlagKey.NewDevicePermanentDismiss to false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE = DebugMenuState(
|
||||
|
|
|
@ -264,6 +264,16 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to new device two factor works as expected:
|
||||
rootNavStateFlow.value =
|
||||
RootNavState.NewDeviceTwoFactorNotice(email = "example@bitwarden.com")
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "new_device_notice/example@bitwarden.com",
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { authStateFlow } returns mutableAuthStateFlow
|
||||
every { showWelcomeCarousel } returns false
|
||||
every { checkUserNeedsNewDeviceTwoFactorNotice() } returns false
|
||||
}
|
||||
|
||||
private val mockAuthRepository = mockk<AuthRepository>(relaxed = true)
|
||||
|
@ -1333,6 +1334,45 @@ 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,
|
||||
|
|
Loading…
Add table
Reference in a new issue