[PM-8217] New device notice email access UI (#4400)

This commit is contained in:
André Bispo
2024-12-20 16:53:30 +00:00
committed by GitHub
parent 6c355ae5b7
commit a7939414ae
9 changed files with 679 additions and 1 deletions

View File

@@ -0,0 +1,56 @@
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.x8bit.bitwarden.data.platform.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(
onNavigateToTwoFactorOptions: () -> Unit,
) {
composableWithSlideTransitions(
route = NEW_DEVICE_NOTICE_EMAIL_ACCESS_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
),
) {
NewDeviceNoticeEmailAccessScreen(
onNavigateToTwoFactorOptions = onNavigateToTwoFactorOptions,
)
}
}

View File

@@ -0,0 +1,190 @@
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.height
import androidx.compose.foundation.layout.imePadding
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.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.NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
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.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The top level composable for the new device notice email access screen.
*/
@Composable
fun NewDeviceNoticeEmailAccessScreen(
onNavigateToTwoFactorOptions: () -> Unit,
viewModel: NewDeviceNoticeEmailAccessViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
NavigateToTwoFactorOptions -> onNavigateToTwoFactorOptions()
}
}
BitwardenScaffold {
NewDeviceNoticeEmailAccessContent(
email = state.email,
isEmailAccessEnabled = state.isEmailAccessEnabled,
onEmailAccessToggleChanged = remember(viewModel) {
{ newState ->
viewModel.trySendAction(EmailAccessToggle(isEnabled = newState))
}
},
onContinueClick = { viewModel.trySendAction(ContinueClick) },
)
}
}
@Composable
private fun NewDeviceNoticeEmailAccessContent(
email: String,
isEmailAccessEnabled: Boolean,
onEmailAccessToggleChanged: (Boolean) -> Unit,
onContinueClick: () -> 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(
email = email,
isEmailAccessEnabled = isEmailAccessEnabled,
onEmailAccessToggleChanged = onEmailAccessToggleChanged,
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenFilledButton(
label = stringResource(R.string.continue_text),
onClick = onContinueClick,
modifier = Modifier
.fillMaxSize()
.imePadding(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
/**
* Header content containing the warning icon and title.
*/
@Suppress("MaxLineLength")
@Composable
private fun ColumnScope.HeaderContent() {
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,
)
}
/**
* 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 = createAnnotatedString(
mainString = stringResource(
R.string.do_you_have_reliable_access_to_your_email,
email,
),
mainStringStyle = SpanStyle(
color = BitwardenTheme.colorScheme.text.primary,
fontSize = BitwardenTheme.typography.bodyLarge.fontSize,
fontWeight = FontWeight.Normal,
),
highlights = listOf(email),
highlightStyle = SpanStyle(
color = BitwardenTheme.colorScheme.text.primary,
fontSize = BitwardenTheme.typography.bodyLarge.fontSize,
fontWeight = FontWeight.Bold,
),
),
)
Column {
BitwardenSwitch(
label = stringResource(id = R.string.yes_i_can_reliably_access_my_email),
isChecked = isEmailAccessEnabled,
onCheckedChange = onEmailAccessToggleChanged,
modifier = Modifier
.testTag("EmailAccessToggle"),
)
}
}
}
@PreviewScreenSizes
@Composable
private fun NewDeviceNoticeEmailAccessScreen_preview() {
BitwardenTheme {
NewDeviceNoticeEmailAccessContent(
email = "test@bitwarden.com",
isEmailAccessEnabled = true,
onEmailAccessToggleChanged = {},
onContinueClick = {},
)
}
}

View File

@@ -0,0 +1,84 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
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
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Manages application state for the new device notice email access screen.
*/
@HiltViewModel
class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<
NewDeviceNoticeEmailAccessState,
NewDeviceNoticeEmailAccessEvent,
NewDeviceNoticeEmailAccessAction,
>(
initialState = savedStateHandle[KEY_STATE]
?: NewDeviceNoticeEmailAccessState(
email = NewDeviceNoticeEmailAccessArgs(savedStateHandle).emailAddress,
isEmailAccessEnabled = false,
),
) {
override fun handleAction(action: NewDeviceNoticeEmailAccessAction) {
when (action) {
ContinueClick -> handleContinueClick()
is EmailAccessToggle -> handleEmailAccessToggle(action)
}
}
private fun handleContinueClick() {
// TODO PM-8217: update new device notice status and navigate accordingly
sendEvent(NavigateToTwoFactorOptions)
}
private fun handleEmailAccessToggle(action: EmailAccessToggle) {
mutableStateFlow.update {
it.copy(isEmailAccessEnabled = action.isEnabled)
}
}
}
/**
* 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()
}
/**
* 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()
}

View File

@@ -130,6 +130,8 @@ fun @receiver:StringRes Int.asText(vararg args: Any): Text = ResArgsText(this, a
* Create an [AnnotatedString] with highlighted parts.
* @param mainString the full string
* @param highlights parts of the mainString that will be highlighted
* @param highlightStyle the style to apply to the highlights
* @param mainStringStyle the style to apply to the mainString
* @param tag the tag that will be used for the annotation
*/
@Composable
@@ -137,12 +139,13 @@ fun createAnnotatedString(
mainString: String,
highlights: List<String>,
highlightStyle: SpanStyle = bitwardenClickableTextSpanStyle,
mainStringStyle: SpanStyle = bitwardenDefaultSpanStyle,
tag: String? = null,
): AnnotatedString {
return buildAnnotatedString {
append(mainString)
addStyle(
style = bitwardenDefaultSpanStyle,
style = mainStringStyle,
start = 0,
end = mainString.length,
)

View File

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

View File

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

View File

@@ -1098,6 +1098,10 @@ Do you want to switch to this account?</string>
<string name="copy_email">Copy email</string>
<string name="copy_phone">Copy phone number</string>
<string name="copy_address">Copy address</string>
<string name="important_notice">Important notice</string>
<string name="bitwarden_will_soon_send_a_code_to_your_account_email_to_verify_logins_from_new_devices_in_february">Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025.</string>
<string name="do_you_have_reliable_access_to_your_email">Do you have reliable access to your email, %1$s? </string>
<string name="yes_i_can_reliably_access_my_email">Yes, I can reliably access my email</string>
<string name="biometrics_no_longer_supported_title">Biometrics are no longer supported on this device</string>
<string name="biometrics_no_longer_supported">Youve been logged out because your devices biometrics dont meet the latest security requirements. To update settings, log in once again or contact your administrator for access.</string>
<string name="cxp_import">CXP Import</string>

View File

@@ -0,0 +1,85 @@
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 com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Before
import org.junit.Test
class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<NewDeviceNoticeEmailAccessEvent>()
private var onNavigateToTwoFactorOptionsCalled = false
private val viewModel = mockk<NewDeviceNoticeEmailAccessViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
}
@Before
fun setUp() {
composeTestRule.setContent {
NewDeviceNoticeEmailAccessScreen(
onNavigateToTwoFactorOptions = { onNavigateToTwoFactorOptionsCalled = true },
viewModel = viewModel,
)
}
}
@Suppress("MaxLineLength")
@Test
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 onNavigateToTwoFactorOptions`() {
mutableEventFlow.tryEmit(NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions)
assertTrue(onNavigateToTwoFactorOptionsCalled)
}
}
private const val EMAIL = "active@bitwarden.com"
private val DEFAULT_STATE =
NewDeviceNoticeEmailAccessState(
email = EMAIL,
isEmailAccessEnabled = false,
)

View File

@@ -0,0 +1,59 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
@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 `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 with valid email should emit NavigateToTwoFactorOptions`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.ContinueClick)
assertEquals(
NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions,
awaitItem(),
)
}
}
private fun createViewModel(
savedStateHandle: SavedStateHandle = SavedStateHandle().also {
it["email_address"] = EMAIL
},
): NewDeviceNoticeEmailAccessViewModel = NewDeviceNoticeEmailAccessViewModel(
savedStateHandle = savedStateHandle,
)
}
private const val EMAIL = "active@bitwarden.com"
private val DEFAULT_STATE =
NewDeviceNoticeEmailAccessState(
email = EMAIL,
isEmailAccessEnabled = false,
)