[PM-6702] Add Check your email screen used for cloud registration flow.

This commit is contained in:
André Bispo
2024-06-19 16:02:40 +01:00
parent 53430cdf8a
commit fa8a5b80a2
14 changed files with 516 additions and 24 deletions

View File

@@ -6,6 +6,8 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.navOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.auth.feature.checkemail.checkEmailDestination
import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination
@@ -59,8 +61,16 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
onNavigateToCompleteRegistration = { emailAddress, verificationToken, captchaToken ->
navController.navigateToCompleteRegistration()
},
onNavigateToCheckEmail = {emailAddress ->
navController.navigateToCheckEmail(emailAddress)
},
onNavigateToEnvironment = { navController.navigateToEnvironment() }
)
checkEmailDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateBackToLanding = {
navController.popBackStack(route = LANDING_ROUTE, inclusive = false)
})
completeRegistrationDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToLogin = { emailAddress, captchaToken ->

View File

@@ -0,0 +1,51 @@
package com.x8bit.bitwarden.ui.auth.feature.checkemail
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: String = "email"
private const val CHECK_EMAIL_ROUTE: String = "check_email/{$EMAIL_ADDRESS}"
/**
* Navigate to the check email screen.
*/
fun NavController.navigateToCheckEmail(emailAddress: String, navOptions: NavOptions? = null) {
this.navigate("check_email/$emailAddress", navOptions)
}
/**
* Class to retrieve check email arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class CheckEmailArgs(
val emailAddress: String
) {
constructor(savedStateHandle: SavedStateHandle) : this(
emailAddress = checkNotNull(savedStateHandle.get<String>(EMAIL_ADDRESS)),
)
}
/**
* Add the check email screen to the nav graph.
*/
fun NavGraphBuilder.checkEmailDestination(
onNavigateBack: () -> Unit,
onNavigateBackToLanding: () -> Unit,
) {
composableWithSlideTransitions(
route = CHECK_EMAIL_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
)
) {
CheckEmailScreen(
onNavigateBack = onNavigateBack,
onNavigateBackToLanding = onNavigateBackToLanding
)
}
}

View File

@@ -0,0 +1,230 @@
package com.x8bit.bitwarden.ui.auth.feature.checkemail
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
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.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
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.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Top level composable for the check email screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun CheckEmailScreen(
onNavigateBack: () -> Unit,
onNavigateBackToLanding: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: CheckEmailViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel) { event ->
when (event) {
is CheckEmailEvent.NavigateBack -> {
onNavigateBack.invoke()
}
is CheckEmailEvent.NavigateToEmailApp -> {
val intent = Intent(Intent.ACTION_SENDTO)
intent.setData(Uri.parse("mailto:"))
intentManager.startActivity(intent)
}
is CheckEmailEvent.NavigateBackToLanding -> {
onNavigateBackToLanding.invoke()
}
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.create_account),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(CheckEmailAction.CloseTap) }
}
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(32.dp))
Image(
painter = rememberVectorPainter(id = R.drawable.email_check),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(horizontal = 16.dp)
.height(112.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = stringResource(id = R.string.check_your_email),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.padding(horizontal = 24.dp)
.wrapContentHeight()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
val descriptionAnnotatedString = CreateAnnotatedString(
mainText = stringResource(id = R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account, state.email),
highlightText = state.email,
highlightSpanStyle = SpanStyle(
color = MaterialTheme.colorScheme.onSurface
)
)
Text(
text = descriptionAnnotatedString,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth()
.wrapContentHeight(),
)
Spacer(modifier = Modifier.height(32.dp))
BitwardenFilledButton(
label = stringResource(id = R.string.open_email_app),
onClick = remember(viewModel) {
{ viewModel.trySendAction(CheckEmailAction.OpenEmailTap) }
},
modifier = Modifier
.testTag("OpenEmailApp")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(32.dp))
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val goBackAnnotatedString = CreateAnnotatedString(
mainText = stringResource(id = R.string.no_email_go_back_to_edit_your_email_address),
highlightText = stringResource(id = R.string.go_back)
)
ClickableText(
text = goBackAnnotatedString,
onClick = {
goBackAnnotatedString
.getStringAnnotations("URL", it, it)
.firstOrNull()?.let {
viewModel.trySendAction(CheckEmailAction.CloseTap)
}
}
)
Spacer(modifier = Modifier.height(32.dp))
val logInAnnotatedString = CreateAnnotatedString(
mainText = stringResource(id = R.string.or_log_in_you_may_already_have_an_account),
highlightText = stringResource(id = R.string.log_in)
)
ClickableText(
text = logInAnnotatedString,
onClick = {
logInAnnotatedString
.getStringAnnotations("URL", it, it)
.firstOrNull()?.let {
viewModel.trySendAction(CheckEmailAction.LoginTap)
}
}
)
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@Composable
private fun CreateAnnotatedString(
mainText: String,
highlightText: String,
mainSpanStyle: SpanStyle = SpanStyle(
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.bodyMedium.fontSize
),
highlightSpanStyle: SpanStyle = SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontWeight = FontWeight.Bold
)
): AnnotatedString {
return buildAnnotatedString {
val startIndex = mainText.indexOf(highlightText, ignoreCase = true)
val endIndex = startIndex + highlightText.length
append(mainText)
addStyle(
style = mainSpanStyle,
start = 0,
end = mainText.length
)
addStyle(
style = highlightSpanStyle,
start = startIndex,
end = endIndex
)
addStringAnnotation(
tag = "URL",
annotation = highlightText,
start = startIndex,
end = endIndex
)
}
}

View File

@@ -0,0 +1,93 @@
package com.x8bit.bitwarden.ui.auth.feature.checkemail
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Models logic for the check email screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class CheckEmailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<CheckEmailState, CheckEmailEvent, CheckEmailAction>(
initialState = savedStateHandle[KEY_STATE]
?: CheckEmailState(
email = CheckEmailArgs(savedStateHandle).emailAddress
),
) {
init {
// As state updates, write to saved state handle:
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
}
override fun handleAction(action: CheckEmailAction) {
when (action) {
CheckEmailAction.CloseTap -> sendEvent(CheckEmailEvent.NavigateBack)
CheckEmailAction.LoginTap -> sendEvent(CheckEmailEvent.NavigateBackToLanding)
CheckEmailAction.OpenEmailTap -> sendEvent(CheckEmailEvent.NavigateToEmailApp)
}
}
}
/**
* UI state for the check email screen.
*/
@Parcelize
data class CheckEmailState(
val email: String
) : Parcelable {
}
/**
* Models events for the check email screen.
*/
sealed class CheckEmailEvent {
/**
* Navigate back to previous screen.
*/
data object NavigateBack : CheckEmailEvent()
/**
* Navigate to email app.
*/
data object NavigateToEmailApp : CheckEmailEvent()
/**
* Navigate to email app.
*/
data object NavigateBackToLanding : CheckEmailEvent()
}
/**
* Models actions for the check email screen.
*/
sealed class CheckEmailAction {
/**
* User tapped close.
*/
data object CloseTap : CheckEmailAction()
/**
* User tapped log in.
*/
data object LoginTap : CheckEmailAction()
/**
* User tapped open email.
*/
data object OpenEmailTap : CheckEmailAction()
}

View File

@@ -57,7 +57,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Top level composable for the create account screen.
* Top level composable for the complete registration screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")

View File

@@ -350,7 +350,7 @@ class CompleteRegistrationViewModel @Inject constructor(
}
/**
* UI state for the create account screen.
* UI state for the complete registration screen.
*/
@Parcelize
data class CompleteRegistrationState(
@@ -394,7 +394,7 @@ data class CompleteRegistrationState(
}
/**
* Models dialogs that can be displayed on the create account screen.
* Models dialogs that can be displayed on the complete registration screen.
*/
sealed class CompleteRegistrationDialog : Parcelable {
/**
@@ -423,7 +423,7 @@ sealed class CompleteRegistrationDialog : Parcelable {
}
/**
* Models events for the create account screen.
* Models events for the complete registration screen.
*/
sealed class CompleteRegistrationEvent {
@@ -452,7 +452,7 @@ sealed class CompleteRegistrationEvent {
}
/**
* Models actions for the create account screen.
* Models actions for the complete registration screen.
*/
sealed class CompleteRegistrationAction {
/**

View File

@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount
package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState

View File

@@ -49,6 +49,7 @@ 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.completeregistration.PasswordStrengthIndicator
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick

View File

@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange

View File

@@ -24,6 +24,7 @@ fun NavGraphBuilder.startRegistrationDestination(
verificationToken: String,
captchaToken: String
) -> Unit,
onNavigateToCheckEmail: (email: String) -> Unit,
onNavigateToEnvironment: () -> Unit,
) {
composableWithSlideTransitions(
@@ -32,6 +33,7 @@ fun NavGraphBuilder.startRegistrationDestination(
StartRegistrationScreen(
onNavigateBack = onNavigateBack,
onNavigateToCompleteRegistration = onNavigateToCompleteRegistration,
onNavigateToCheckEmail = onNavigateToCheckEmail,
onNavigateToEnvironment = onNavigateToEnvironment,
)
}

View File

@@ -69,7 +69,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Top level composable for the create account screen.
* Top level composable for the start registration screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@@ -80,6 +80,7 @@ fun StartRegistrationScreen(
emailAddress: String,
verificationToken: String,
captchaToken: String) -> Unit,
onNavigateToCheckEmail: (email: String) -> Unit,
onNavigateToEnvironment: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: StartRegistrationViewModel = hiltViewModel(),
@@ -117,6 +118,12 @@ fun StartRegistrationScreen(
)
}
is StartRegistrationEvent.NavigateToCheckEmail -> {
onNavigateToCheckEmail(
event.email
)
}
StartRegistrationEvent.NavigateToEnvironment -> onNavigateToEnvironment()
}
}
@@ -278,8 +285,6 @@ private fun TermsAndPrivacyText(
start = startIndexPrivacy,
end = endIndexPrivacy
)
// attach a string annotation that stores a URL to the text "link"
addStringAnnotation(
tag = "URL",
annotation = strTerms,
@@ -292,7 +297,6 @@ private fun TermsAndPrivacyText(
start = startIndexPrivacy,
end = endIndexPrivacy
)
}
Row(
horizontalArrangement = Arrangement.Start,
@@ -323,7 +327,6 @@ private fun TermsAndPrivacyText(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Suppress("LongMethod")
@Composable
private fun ReceiveMarketingEmailsSwitch(
@@ -334,7 +337,7 @@ private fun ReceiveMarketingEmailsSwitch(
val annotatedLinkString: AnnotatedString = buildAnnotatedString {
val strMarketingEmail = stringResource(id = R.string.get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time)
val strUnsubscribe = stringResource(id = R.string.unsubscribe)
val startIndexUnsubscribe = strMarketingEmail.indexOf(strUnsubscribe)
val startIndexUnsubscribe = strMarketingEmail.indexOf(strUnsubscribe, ignoreCase = true)
val endIndexUnsubscribe = startIndexUnsubscribe + strUnsubscribe.length
append(strMarketingEmail)
addStyle(

View File

@@ -31,7 +31,7 @@ import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Models logic for the create account screen.
* Models logic for the start registration screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
@@ -98,8 +98,8 @@ class StartRegistrationViewModel @Inject constructor(
is StartRegistrationAction.EnvironmentTypeSelect -> handleEnvironmentTypeSelect(action)
is StartRegistrationAction.Internal.UpdatedEnvironmentReceive -> {
handleUpdatedEnvironmentReceive(action)
}
handleUpdatedEnvironmentReceive(action)
}
}
}
@@ -234,11 +234,16 @@ class StartRegistrationViewModel @Inject constructor(
*/
viewModelScope.launch {
sendEvent(StartRegistrationEvent.NavigateToCompleteRegistration(
email = state.emailInput,
verificationToken = "",
captchaToken = ""
))
if (environmentRepository.environment.type == Environment.Type.US || environmentRepository.environment.type == Environment.Type.EU)
sendEvent(StartRegistrationEvent.NavigateToCheckEmail(
email = state.emailInput
))
else
sendEvent(StartRegistrationEvent.NavigateToCompleteRegistration(
email = state.emailInput,
verificationToken = "",
captchaToken = ""
))
}
}
}
@@ -268,7 +273,7 @@ class StartRegistrationViewModel @Inject constructor(
}
/**
* UI state for the create account screen.
* UI state for the start registration screen.
*/
@Parcelize
data class StartRegistrationState(
@@ -283,7 +288,7 @@ data class StartRegistrationState(
}
/**
* Models dialogs that can be displayed on the create account screen.
* Models dialogs that can be displayed on the start registration screen.
*/
sealed class StartRegistrationDialog : Parcelable {
/**
@@ -300,7 +305,7 @@ sealed class StartRegistrationDialog : Parcelable {
}
/**
* Models events for the create account screen.
* Models events for the start registration screen.
*/
sealed class StartRegistrationEvent {
@@ -328,6 +333,13 @@ sealed class StartRegistrationEvent {
val captchaToken: String,
) : StartRegistrationEvent()
/**
* Navigates to the complete registration screen.
*/
data class NavigateToCheckEmail(
val email: String
) : StartRegistrationEvent()
/**
* Navigate to terms and conditions.
*/
@@ -350,7 +362,7 @@ sealed class StartRegistrationEvent {
}
/**
* Models actions for the create account screen.
* Models actions for the start registration screen.
*/
sealed class StartRegistrationAction {
/**

View File

@@ -0,0 +1,84 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="413dp"
android:height="114dp"
android:viewportWidth="413"
android:viewportHeight="114">
<group>
<clip-path
android:pathData="M134.84,0.57h143.82v112.71h-143.82z"/>
<path
android:pathData="M260.14,44.62V59.04M192.86,14.74L201.04,8.64C204.02,6.42 208.12,6.45 211.07,8.73L212.84,10.09M164.61,35.82L156.15,42.14C154.05,43.7 152.82,46.17 152.82,48.79V100.48C152.82,105.07 156.53,108.78 161.11,108.78H251.85C256.43,108.78 260.14,105.07 260.14,100.48V71.05"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M165.38,54.52V17.83C165.38,16.3 166.61,15.07 168.14,15.07H206.8"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M175.32,27.21H194.11"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M175.32,38.39H193.56"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M175.32,49.57H196.57"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M175.32,60.75H203.13"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M257.86,106.5L223.49,74.77C220.93,72.41 217.59,71.1 214.11,71.1H197.24C193.65,71.1 190.2,72.5 187.62,75L155.1,106.5"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"/>
<path
android:pathData="M220.36,71.58L231.04,65.46"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M153.86,48.45L192.59,71.58"
android:strokeWidth="2.77"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M260.35,35.09C260.35,51.78 246.82,65.3 230.13,65.3C213.45,65.3 199.92,51.78 199.92,35.09C199.92,18.4 213.45,4.88 230.13,4.88C246.82,4.88 260.35,18.4 260.35,35.09Z"
android:strokeLineJoin="round"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M256.3,35.09C256.3,49.44 244.53,61.07 230.02,61.07M230.02,9.11C215.5,9.11 203.73,20.74 203.73,35.09"
android:strokeLineJoin="round"
android:strokeWidth="1.38289"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
<path
android:pathData="M254.25,53.55L258.87,58.17L276.21,75.51C277.56,76.86 277.56,79.05 276.21,80.4L275.49,81.12C274.14,82.47 271.95,82.47 270.6,81.12L253.26,63.78L248.64,59.16"
android:strokeLineJoin="round"
android:strokeWidth="2.76577"
android:fillColor="#00000000"
android:strokeColor="#175DDC"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -918,5 +918,10 @@ Do you want to switch to this account?</string>
<string name="by_continuing_you_agree_to_the_terms_of_service_and_privacy_policy">By continuing, you agree to the Terms of Service and Privacy Policy</string>
<string name="set_password">Set password</string>
<string name="unsubscribe">Unsubscribe</string>
<string name="check_your_email">Check your email</string>
<string name="open_email_app">Open email app</string>
<string name="go_back">Go back</string>
<string name="no_email_go_back_to_edit_your_email_address">No email? Go back to edit your email address.</string>
<string name="or_log_in_you_may_already_have_an_account">Or log in, you may already have an account.</string>
<string name="get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time">Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time.</string>
</resources>